mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
fix: normalize dashboard label rendering (#1732)
* fix: normalize tool call rendering Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: simplify tool call labels Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> * fix: capitalize dashboard UI labels Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> --------- Co-authored-by: Ramon Nogueira <270434257+ramon-langchain@users.noreply.github.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> (cherry picked from commit d714586ce6928e4848214a8fce78cc4597ff2684) Co-authored-by: Ramon Nogueira <ramon.nogueira@langchain.dev>
This commit is contained in:
parent
ead210927d
commit
58941d1edd
9 changed files with 149 additions and 75 deletions
|
|
@ -61,6 +61,10 @@ function triToBool(value: TriState): boolean | undefined {
|
|||
return undefined
|
||||
}
|
||||
|
||||
function displayStatus(status: AgentStatus): string {
|
||||
return STATUS_OPTIONS.find((option) => option.value === status)?.label ?? status
|
||||
}
|
||||
|
||||
export function AgentsThreadsPage({
|
||||
filters,
|
||||
onFiltersChange,
|
||||
|
|
@ -279,8 +283,8 @@ function ThreadListItem({ thread }: { thread: AgentThread }) {
|
|||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm text-[var(--ui-text)]">{thread.title}</p>
|
||||
<p className="truncate text-[11px] text-[var(--ui-text-dim)]">
|
||||
{thread.repoFullName || "no repo"} · {thread.status}
|
||||
{isResolved ? " · resolved" : ""}
|
||||
{thread.repoFullName || "No repo"} · {displayStatus(thread.status)}
|
||||
{isResolved ? " · Resolved" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -75,11 +75,11 @@ export function WorkflowApprovalCard({
|
|||
</div>
|
||||
<p className="text-xs text-[var(--ui-text-dim)]">
|
||||
{approval.repo || "Repository"} on{" "}
|
||||
{approval.branch || "current branch"} ·{" "}
|
||||
{approval.branch || "Current branch"} ·{" "}
|
||||
{shortSha(approval.baseSha)} → {shortSha(approval.headSha)}
|
||||
</p>
|
||||
<p className="font-mono text-[0.68rem] break-all text-[var(--ui-text-dim)]">
|
||||
fingerprint: {approval.fingerprint}
|
||||
Fingerprint: {approval.fingerprint}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap gap-2">
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ export const CloudPromptBar = memo(function CloudPromptBarComponent({
|
|||
>
|
||||
<img
|
||||
src={`data:${image.mimeType};base64,${image.base64}`}
|
||||
alt={image.fileName || "pending image"}
|
||||
alt={image.fileName || "Pending image"}
|
||||
className="size-16 rounded-lg border border-[var(--ui-border)] object-cover"
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
39
ui/src/features/agents/components/chat/ToolExecution.test.ts
Normal file
39
ui/src/features/agents/components/chat/ToolExecution.test.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatToolDisplay } from "./toolExecutionDisplay";
|
||||
|
||||
describe("formatToolDisplay", () => {
|
||||
const projectPath = "/workspace/open-swe";
|
||||
|
||||
it("renders read_file with the file_path alias consistently", () => {
|
||||
expect(
|
||||
formatToolDisplay(
|
||||
"read_file /workspace/open-swe/AGENTS.md",
|
||||
"read",
|
||||
{ file_path: "/workspace/open-swe/AGENTS.md" },
|
||||
projectPath,
|
||||
),
|
||||
).toBe("Read AGENTS.md");
|
||||
});
|
||||
|
||||
it("renders ls as a list operation with a relative path", () => {
|
||||
expect(
|
||||
formatToolDisplay(
|
||||
"ls /workspace/open-swe/ui",
|
||||
"read",
|
||||
{ path: "/workspace/open-swe/ui" },
|
||||
projectPath,
|
||||
),
|
||||
).toBe("List ui");
|
||||
});
|
||||
|
||||
it("renders search tools with their pattern", () => {
|
||||
expect(
|
||||
formatToolDisplay("grep", "search", { pattern: "tool_calls" }, projectPath),
|
||||
).toBe('Search "tool_calls"');
|
||||
});
|
||||
|
||||
it("normalizes write_todos", () => {
|
||||
expect(formatToolDisplay("write todos", "other", {}, projectPath)).toBe("Update todos");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { MultiFileDiff } from "@pierre/diffs/react";
|
||||
import { DiffView } from "./DiffView";
|
||||
import type { AcpToolKind, ToolExecutionChunk } from "@/features/agents/lib/types";
|
||||
import { formatToolDisplay } from "./toolExecutionDisplay";
|
||||
import type { ToolExecutionChunk } from "@/features/agents/lib/types";
|
||||
import { useDiffOptions } from "@/features/agents/utils/diffUtils";
|
||||
import { countLineChanges } from "@/features/agents/utils/diffStats";
|
||||
|
||||
|
|
@ -27,60 +28,6 @@ function getFileName(path: string): string {
|
|||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
|
||||
function formatToolDisplay(
|
||||
title: string,
|
||||
toolKind: AcpToolKind,
|
||||
input: Record<string, unknown> | undefined,
|
||||
projectPath?: string,
|
||||
): string {
|
||||
const path = input?.path as string | undefined;
|
||||
const pattern = input?.pattern as string | undefined;
|
||||
const query = input?.query as string | undefined;
|
||||
const url = input?.url as string | undefined;
|
||||
const command = input?.command as string | undefined;
|
||||
|
||||
switch (toolKind) {
|
||||
case "read": {
|
||||
if (path) {
|
||||
const displayPath = stripProjectPath(path, projectPath);
|
||||
return `Read(${displayPath})`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
case "search": {
|
||||
if (pattern) {
|
||||
const truncated = pattern.length > 40 ? pattern.slice(0, 40) + "..." : pattern;
|
||||
return `Search("${truncated}")`;
|
||||
}
|
||||
if (query) {
|
||||
return `Search("${query.slice(0, 40)}${query.length > 40 ? "..." : ""}")`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
case "fetch": {
|
||||
if (url) {
|
||||
return `Fetch(${url.slice(0, 50)}${url.length > 50 ? "..." : ""})`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
case "execute": {
|
||||
if (command) {
|
||||
const truncated = command.length > 60 ? command.slice(0, 60) + "..." : command;
|
||||
return `Shell(${truncated})`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
case "edit":
|
||||
case "delete":
|
||||
case "move":
|
||||
return title;
|
||||
case "think":
|
||||
return "Thinking...";
|
||||
default:
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
const InlineDiffCollapsible = memo(function InlineDiffCollapsible({
|
||||
filePath,
|
||||
fileName,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import type { AcpToolKind } from "@/features/agents/lib/types";
|
||||
|
||||
function stripProjectPath(path: string, projectPath?: string): string {
|
||||
if (!projectPath || !path.startsWith(projectPath)) return path;
|
||||
const relative = path.slice(projectPath.length);
|
||||
return relative.replace(/^\/+/, "") || ".";
|
||||
}
|
||||
|
||||
function firstStringArg(
|
||||
input: Record<string, unknown> | undefined,
|
||||
keys: Array<string>,
|
||||
): string | undefined {
|
||||
if (!input) return undefined;
|
||||
for (const key of keys) {
|
||||
const value = input[key];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function truncateMiddle(value: string, maxLength: number): string {
|
||||
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
||||
}
|
||||
|
||||
function normalizedToolName(title: string): string {
|
||||
return title.trim().split(/\s+/, 1)[0]?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
function humanizeToolTitle(title: string): string {
|
||||
return title.replace(/_/g, " ").trim() || "Tool";
|
||||
}
|
||||
|
||||
export function formatToolDisplay(
|
||||
title: string,
|
||||
toolKind: AcpToolKind,
|
||||
input: Record<string, unknown> | undefined,
|
||||
projectPath?: string,
|
||||
): string {
|
||||
const toolName = normalizedToolName(title);
|
||||
const path = firstStringArg(input, ["path", "file_path", "target_file"]);
|
||||
const pattern = firstStringArg(input, ["pattern"]);
|
||||
const query = firstStringArg(input, ["query"]);
|
||||
const url = firstStringArg(input, ["url"]);
|
||||
const command = firstStringArg(input, ["command"]);
|
||||
|
||||
switch (toolKind) {
|
||||
case "read": {
|
||||
if (path) {
|
||||
const displayPath = stripProjectPath(path, projectPath);
|
||||
return toolName === "ls" ? `List ${displayPath}` : `Read ${displayPath}`;
|
||||
}
|
||||
return humanizeToolTitle(title);
|
||||
}
|
||||
case "search": {
|
||||
if (pattern) return `Search "${truncateMiddle(pattern, 40)}"`;
|
||||
if (query) return `Search "${truncateMiddle(query, 40)}"`;
|
||||
if (path) return `Search ${stripProjectPath(path, projectPath)}`;
|
||||
return humanizeToolTitle(title);
|
||||
}
|
||||
case "fetch": {
|
||||
if (url) return `Fetch ${truncateMiddle(url, 50)}`;
|
||||
return humanizeToolTitle(title);
|
||||
}
|
||||
case "execute": {
|
||||
if (command) return `Shell ${truncateMiddle(command, 60)}`;
|
||||
return humanizeToolTitle(title);
|
||||
}
|
||||
case "edit":
|
||||
case "delete":
|
||||
case "move":
|
||||
return humanizeToolTitle(title);
|
||||
case "think":
|
||||
return "Thinking...";
|
||||
default: {
|
||||
if (toolName === "write_todos" || title.toLowerCase().startsWith("write todos")) {
|
||||
return "Update todos";
|
||||
}
|
||||
if (toolName === "ls" && path) return `List ${stripProjectPath(path, projectPath)}`;
|
||||
return humanizeToolTitle(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,17 +3,17 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { formatElapsed } from "@/lib/utils";
|
||||
|
||||
const BUSY_TEXTS: Array<{ present: string; past: string }> = [
|
||||
{ present: "vibing...", past: "Vibed" },
|
||||
{ present: "noodling...", past: "Noodled" },
|
||||
{ present: "pondering...", past: "Pondered" },
|
||||
{ present: "thinking really hard...", past: "Thought really hard" },
|
||||
{ present: "spinning up...", past: "Spun up" },
|
||||
{ present: "connecting the dots...", past: "Connected the dots" },
|
||||
{ present: "brewing ideas...", past: "Brewed ideas" },
|
||||
{ present: "cooking...", past: "Cooked" },
|
||||
{ present: "crunching...", past: "Crunched" },
|
||||
{ present: "scheming...", past: "Schemed" },
|
||||
{ present: "processing...", past: "Processed" },
|
||||
{ present: "Vibing...", past: "Vibed" },
|
||||
{ present: "Noodling...", past: "Noodled" },
|
||||
{ present: "Pondering...", past: "Pondered" },
|
||||
{ present: "Thinking really hard...", past: "Thought really hard" },
|
||||
{ present: "Spinning up...", past: "Spun up" },
|
||||
{ present: "Connecting the dots...", past: "Connected the dots" },
|
||||
{ present: "Brewing ideas...", past: "Brewed ideas" },
|
||||
{ present: "Cooking...", past: "Cooked" },
|
||||
{ present: "Crunching...", past: "Crunched" },
|
||||
{ present: "Scheming...", past: "Schemed" },
|
||||
{ present: "Processing...", past: "Processed" },
|
||||
];
|
||||
|
||||
const THINKING_SETTLE_MS = 300;
|
||||
|
|
|
|||
|
|
@ -528,7 +528,7 @@ export const PromptBar = memo(function PromptBar({
|
|||
<div key={i} className="relative group">
|
||||
<img
|
||||
src={`data:${img.mimeType};base64,${img.base64}`}
|
||||
alt={img.fileName || "pending image"}
|
||||
alt={img.fileName || "Pending image"}
|
||||
className="w-16 h-16 object-cover rounded border border-gray-600"
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ import type { AssembledToolCall, SubagentDiscoverySnapshot } from "@langchain/re
|
|||
|
||||
import type { Chunk, DiffData, Message, ToolExecutionChunk } from "./types";
|
||||
|
||||
const READ_TOOLS = new Set(["read_file", "read", "glob", "grep"]);
|
||||
const READ_TOOLS = new Set(["read_file", "read", "ls"]);
|
||||
const EDIT_TOOLS = new Set(["write_file", "edit_file", "str_replace", "write", "edit", "patch"]);
|
||||
const EXECUTE_TOOLS = new Set(["execute", "bash", "shell", "run_terminal_cmd"]);
|
||||
const SEARCH_TOOLS = new Set(["glob", "grep", "web_search", "fetch_url", "search"]);
|
||||
const SEARCH_TOOLS = new Set(["glob", "grep", "web_search", "search"]);
|
||||
const FETCH_TOOLS = new Set(["fetch", "fetch_url", "http_request"]);
|
||||
const INTERNAL_TOOLS = new Set(["confirming_completion", "no_op"]);
|
||||
|
||||
type ToolKind = ToolExecutionChunk["toolKind"];
|
||||
|
|
@ -19,14 +20,15 @@ function toolKind(name: string): ToolKind {
|
|||
if (lowered === "task") return "task";
|
||||
if (lowered === "slack_thread_reply") return "slack";
|
||||
if (lowered === "linear_comment") return "linear";
|
||||
if (lowered === "write_todos") return "other";
|
||||
if (EDIT_TOOLS.has(lowered) || ["edit", "write", "replace"].some((t) => lowered.includes(t))) {
|
||||
return "edit";
|
||||
}
|
||||
if (EXECUTE_TOOLS.has(lowered)) return "execute";
|
||||
if (FETCH_TOOLS.has(lowered)) return "fetch";
|
||||
if (SEARCH_TOOLS.has(lowered)) return "search";
|
||||
if (READ_TOOLS.has(lowered) || lowered.includes("read")) return "read";
|
||||
if (lowered === "think") return "think";
|
||||
if (["fetch", "fetch_url", "http_request"].includes(lowered)) return "fetch";
|
||||
return "other";
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue