diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/chat/logic/tool-summary.test.ts | 81 | ||||
| -rw-r--r-- | src/features/chat/logic/tool-summary.ts | 89 | ||||
| -rw-r--r-- | src/features/chat/ui.test.ts | 10 | ||||
| -rw-r--r-- | src/features/chat/ui/ChatView.test.ts | 66 | ||||
| -rw-r--r-- | src/features/chat/ui/ToolCard.svelte | 27 |
5 files changed, 226 insertions, 47 deletions
diff --git a/src/features/chat/logic/tool-summary.test.ts b/src/features/chat/logic/tool-summary.test.ts index 876ede2..31c7002 100644 --- a/src/features/chat/logic/tool-summary.test.ts +++ b/src/features/chat/logic/tool-summary.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { ToolEntry } from "../../../core/chunks"; -import { viewToolEntry } from "./tool-summary"; +import { toolLabel, viewToolEntry } from "./tool-summary"; function entry( name: string, @@ -23,6 +23,44 @@ const view = (name: string, input: unknown, content: string | null = "ok", isErr viewToolEntry(entry(name, input, content, isError)); const lines = (n: number) => Array.from({ length: n }, (_, i) => `line ${i + 1}`); +describe("tool labels", () => { + it.each([ + ["run_shell", "Run command"], + ["read_file", "Read file"], + ["edit_file", "Edit file"], + ["write_file", "Write file"], + ["web_search", "Web search"], + ["lsp", "Code intelligence"], + ["todo_write", "Update to-dos"], + ["load_skill", "Load skill"], + ["consult_vision", "Ask vision model"], + ["read_image", "Read image"], + ["youtube_transcript", "YouTube transcript"], + ["summon_agent", "Summon agent"], + ["send_agent_message", "Message agent"], + ["read_agent", "Read agent reply"], + ["open_agent", "Open agent"], + ["cancel_agent_message", "Cancel agent message"], + ["list_agents", "List agents"], + ["list_agent_models", "List agent models"], + ])("built-in %s → %s", (name, label) => { + expect(toolLabel(name)).toBe(label); + expect(view(name, {}).label).toBe(label); + expect(view(name, {}).name).toBe(name); + }); + it.each([ + ["github__create_issue", "Github: Create issue"], + ["mcp__github__create_issue", "Github: Create issue"], + ["playwright__browserTakeScreenshot", "Playwright: Browser take screenshot"], + ["scaffold_echo", "Scaffold echo"], + ["fetchUrl", "Fetch url"], + ["some-kebab-tool", "Some kebab tool"], + ["toString", "To string"], + ])("unknown %s is humanized → %s", (name, label) => { + expect(toolLabel(name)).toBe(label); + }); +}); + describe("tool summaries", () => { it.each([ ["run_shell", { command: "bun run test\n --run" }, "bun run test --run"], @@ -156,20 +194,18 @@ describe("tool summaries", () => { ])("generic fallback recognizes %s", (key) => { expect(view("mcp__foo", { [key]: "target" }).callSummary).toBe("target"); }); - it("generic fallback prefers the first meaningful argument, else compact JSON", () => { + it("generic fallback prefers the first meaningful argument, else readable key: value pairs", () => { expect(view("mcp__foo", { command: " ", path: "a", query: "b" }).callSummary).toBe("a"); - expect(view("mcp__foo", { a: 1, b: "x" }).callSummary).toBe('{"a":1,"b":"x"}'); + expect(view("mcp__foo", { a: 1, b: "x" }).callSummary).toBe("a: 1 · b: x"); + // Only nested values → no summary rather than a JSON dump. + expect(view("mcp__foo", { opts: { deep: true } }).callSummary).toBe(""); }); it.each([ "not JSON", "{bad", - null, - undefined, 42, true, [1, "a"], - {}, - "null", '"hi"', ])("tolerates malformed/non-object input %j", (input) => { expect(() => view("read_file", input)).not.toThrow(); @@ -177,6 +213,20 @@ describe("tool summaries", () => { expect(view("read_file", input).callDetail.kind).toBe("fields"); }); it.each([ + null, + undefined, + {}, + [], + "", + " ", + "null", + "{}", + ])("an argument-less call (%j) shows no summary and a 'no arguments' detail — never `{}`", (input) => { + const card = view("list_agent_models", input); + expect(card.callSummary).toBe(""); + expect(card.callDetail).toEqual({ kind: "none" }); + }); + it.each([ "run_shell", "read_file", "edit_file", @@ -190,10 +240,12 @@ describe("tool summaries", () => { "summon_agent", "read_agent", ])("%s falls back safely for missing arguments", (name) => { - for (const input of [null, undefined, {}, "{bad"]) { + for (const input of [null, undefined, {}]) { expect(() => view(name, input)).not.toThrow(); - expect(view(name, input).callSummary.trim()).not.toBe(""); + expect(view(name, input).callSummary).toBe(""); + expect(view(name, input).callDetail).toEqual({ kind: "none" }); } + expect(view(name, "{bad").callSummary).toBe("{bad"); }); it("parses JSON-string inputs", () => { const card = view("run_shell", '{"command":"bun run test"}'); @@ -201,10 +253,11 @@ describe("tool summaries", () => { expect(card.callDetail).toEqual({ kind: "command", text: "bun run test" }); }); it("handles unexpected built-in argument types and prototype-looking tool names", () => { - expect(view("todo_write", { todos: "wrong" }).callSummary).toBe('{"todos":"wrong"}'); + expect(view("todo_write", { todos: "wrong" }).callSummary).toBe("todos: wrong"); expect(view("run_shell", { command: { nested: true } }).callDetail.kind).toBe("fields"); expect(view("constructor", { path: "a" }).callSummary).toBe("a"); - expect(view("__proto__", {}).callSummary).toBe("{}"); + expect(view("__proto__", {}).callSummary).toBe(""); + expect(view("__proto__", {}).label).toBe("Proto"); expect( view("mcp", JSON.parse('{"__proto__":{"command":"bad"},"path":"good"}')).callSummary, ).toBe("good"); @@ -212,7 +265,11 @@ describe("tool summaries", () => { it("does not throw on unserializable input", () => { const cyclic: Record<string, unknown> = {}; cyclic.self = cyclic; - expect(view("mcp", cyclic).callSummary).toBe("[unserializable input]"); + expect(view("mcp", cyclic).callSummary).toBe(""); + expect(view("mcp", cyclic).callDetail).toEqual({ + kind: "fields", + fields: [{ key: "self", value: "[unserializable input]", multiline: false }], + }); expect(() => view("mcp", { integer: 1n })).not.toThrow(); }); it("caps and whitespace-collapses header strings but retains full detail", () => { diff --git a/src/features/chat/logic/tool-summary.ts b/src/features/chat/logic/tool-summary.ts index 3ea4a8e..75682de 100644 --- a/src/features/chat/logic/tool-summary.ts +++ b/src/features/chat/logic/tool-summary.ts @@ -1,7 +1,10 @@ import type { ToolEntry } from "../../../core/chunks"; export interface ToolCardView { + /** The raw wire tool name (tooltip / debugging). */ readonly name: string; + /** English display name, e.g. `run_shell` → "Run command" (see {@link toolLabel}). */ + readonly label: string; readonly status: "pending" | "ok" | "error"; readonly callSummary: string; readonly callBadges: readonly { @@ -12,6 +15,8 @@ export interface ToolCardView { readonly compactResultSummary: string | null; readonly callDetail: | { readonly kind: "unavailable"; readonly text: string } + /** The call took no arguments (`{}` / absent input) — rendered as a muted note. */ + | { readonly kind: "none" } | { readonly kind: "command"; readonly text: string } | { readonly kind: "edit"; @@ -130,6 +135,77 @@ function todoSummary(args: Args): string { const summarizeAgent: CallSummary = (args) => arg(args, "title", "task", "agent", "model"); const summarizeConversation: CallSummary = (args) => arg(args, "conversationId", "message"); +/** English names for Dispatch's built-in tools; anything else goes through {@link humanize}. */ +const TOOL_LABELS: Readonly<Record<string, string>> = { + run_shell: "Run command", + read_file: "Read file", + edit_file: "Edit file", + write_file: "Write file", + web_search: "Web search", + lsp: "Code intelligence", + todo_write: "Update to-dos", + load_skill: "Load skill", + consult_vision: "Ask vision model", + read_image: "Read image", + youtube_transcript: "YouTube transcript", + summon_agent: "Summon agent", + send_agent_message: "Message agent", + read_agent: "Read agent reply", + open_agent: "Open agent", + cancel_agent_message: "Cancel agent message", + list_agents: "List agents", + list_agent_models: "List agent models", +}; + +/** `create_issue` / `createIssue` / `create-issue` → "Create issue". */ +function sentenceCase(raw: string): string { + const words = raw + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .split(/[\s_\-.]+/) + .filter((w) => w !== "") + .map((w) => w.toLowerCase()); + if (words.length === 0) return raw; + const text = words.join(" "); + return text.charAt(0).toUpperCase() + text.slice(1); +} + +/** + * Generic English name for an unknown tool. MCP tools arrive namespaced as + * `<serverId>__<toolName>` (backend `mcp/registry.ts`), shown as "Server: Tool name". + */ +function humanize(name: string): string { + const parts = name.split("__").filter((p) => p !== ""); + if (parts[0]?.toLowerCase() === "mcp" && parts.length > 1) parts.shift(); + if (parts.length >= 2) { + const server = sentenceCase(parts[0] ?? ""); + return `${server}: ${sentenceCase(parts.slice(1).join(" "))}`; + } + return sentenceCase(name); +} + +/** The English display name for a tool (built-in table, else humanized). */ +export function toolLabel(name: string): string { + return Object.hasOwn(TOOL_LABELS, name) ? (TOOL_LABELS[name] ?? name) : humanize(name); +} + +/** True for input that carries no arguments at all (absent, `null`, `{}`, `[]`, blank string). */ +function isEmptyInput(input: unknown): boolean { + if (input === undefined || input === null) return true; + if (typeof input === "string") return input.trim() === ""; + if (Array.isArray(input)) return input.length === 0; + return typeof input === "object" && Object.keys(input).length === 0; +} + +/** Fallback summary: scalar args as `key: value` (never raw JSON braces). */ +function scalarPairs(args: Args): string { + return Object.entries(args) + .filter( + ([, v]) => ["string", "number", "boolean"].includes(typeof v) && String(v).trim() !== "", + ) + .map(([k, v]) => `${k}: ${String(v)}`) + .join(" · "); +} + /** Optional built-in polish; all unknown/MCP names use the same generic path. */ const summaries: Readonly<Record<string, CallSummary>> = { run_shell: (args) => arg(args, "command"), @@ -163,6 +239,7 @@ function detail(name: string, input: unknown, args: Args): ToolCardView["callDet newText: typeof args.newString === "string" ? args.newString : "", }; } + if (isEmptyInput(input)) return { kind: "none" }; const entries = Object.entries(args); return { kind: "fields", @@ -249,7 +326,16 @@ export function viewToolEntry(entry: ToolEntry): ToolCardView { const callSummary = entry.call === null ? "Call in earlier history" - : oneLine(builtInSummary || generic || serialize(input)); + : isEmptyInput(input) + ? "" + : oneLine( + builtInSummary || + generic || + scalarPairs(args) || + // Non-object input (e.g. a bare string); objects with only nested + // values stay unsummarized rather than dumping JSON braces. + (Object.keys(args).length === 0 ? serialize(input) : ""), + ); const callBadges: ToolCardView["callBadges"][number][] = []; if (entry.toolName === "edit_file" && entry.call !== null && !entry.result?.isError) { if (typeof args.oldString === "string" && typeof args.newString === "string") { @@ -265,6 +351,7 @@ export function viewToolEntry(entry: ToolEntry): ToolCardView { result === null ? null : responseSummary(entry.toolName, result.content, result.isError); return { name: entry.toolName, + label: toolLabel(entry.toolName), status: result === null ? "pending" : result.isError ? "error" : "ok", callSummary, callBadges, diff --git a/src/features/chat/ui.test.ts b/src/features/chat/ui.test.ts index c3a76d4..6a00e49 100644 --- a/src/features/chat/ui.test.ts +++ b/src/features/chat/ui.test.ts @@ -108,7 +108,7 @@ describe("ChatView", () => { const { container } = render(ChatView, { props: { chunks } }); await openAllCollapses(container); - expect(screen.getByText("read_file")).toBeInTheDocument(); + expect(screen.getByText("Read file")).toBeInTheDocument(); const pre = screen.getByText((content, element) => { return element?.tagName === "PRE" && content.includes("/tmp/test.txt"); }); @@ -134,7 +134,7 @@ describe("ChatView", () => { const { container } = render(ChatView, { props: { chunks } }); await openAllCollapses(container); - expect(screen.getByText("read_file")).toBeInTheDocument(); + expect(screen.getByText("Read file")).toBeInTheDocument(); expect(screen.getByText("file contents here")).toBeInTheDocument(); }); @@ -262,8 +262,8 @@ describe("ChatView", () => { // Both call names + the available result are shown; the result is absorbed // (no standalone tool-result card). - expect(screen.getByText("read_file")).toBeInTheDocument(); - expect(screen.getByText("list_dir")).toBeInTheDocument(); + expect(screen.getByText("Read file")).toBeInTheDocument(); + expect(screen.getByText("List dir")).toBeInTheDocument(); expect(screen.getByText("contents-of-a")).toBeInTheDocument(); }); @@ -781,7 +781,7 @@ describe("ChatView", () => { const { container } = render(ChatView, { props: { chunks } }); await openAllCollapses(container); - expect(screen.getAllByText("consult_vision").length).toBeGreaterThan(0); + expect(screen.getAllByText("Ask vision model").length).toBeGreaterThan(0); expect(screen.getByText("a red square on a white background")).toBeInTheDocument(); }); diff --git a/src/features/chat/ui/ChatView.test.ts b/src/features/chat/ui/ChatView.test.ts index 9ac4986..4b28b48 100644 --- a/src/features/chat/ui/ChatView.test.ts +++ b/src/features/chat/ui/ChatView.test.ts @@ -135,8 +135,8 @@ describe("ChatView — duplicate toolCallIds", () => { expect(container.querySelectorAll(".chat-bubble")).toHaveLength(2); // Both unique tool names shown; each result rendered once (deduped). - expect(screen.getByText("read_file")).toBeInTheDocument(); - expect(screen.getByText("list_dir")).toBeInTheDocument(); + expect(screen.getByText("Read file")).toBeInTheDocument(); + expect(screen.getByText("List dir")).toBeInTheDocument(); expect(screen.getByText("contents-of-a")).toBeInTheDocument(); expect(screen.getByText("contents-of-b")).toBeInTheDocument(); }); @@ -159,10 +159,11 @@ describe("ChatView — lazy collapse bodies", () => { }, ]; const { container } = render(ChatView, { props: { chunks } }); - expect(screen.getByText("run_shell")).toBeInTheDocument(); + // English display name; the raw wire name stays available as the tooltip. + expect(screen.getByText("Run command")).toHaveAttribute("title", "run_shell"); expect(screen.queryByText("HUGE-OUTPUT")).toBeNull(); - const box = screen.getByRole("checkbox", { name: "Toggle tool call run_shell" }); + const box = screen.getByRole("checkbox", { name: "Toggle tool call Run command" }); await userEvent.click(box); expect(screen.getByText("HUGE-OUTPUT")).toBeInTheDocument(); @@ -211,7 +212,7 @@ describe("ChatView — tool cards", () => { expect(screen.getByText("bun run test")).toBeInTheDocument(); expect(screen.getByText("→ 2 lines")).toBeInTheDocument(); expect(screen.getByLabelText("Succeeded")).toHaveTextContent("✓"); - const box = screen.getByRole("checkbox", { name: "Toggle tool call run_shell" }); + const box = screen.getByRole("checkbox", { name: "Toggle tool call Run command" }); expect(box).not.toBeChecked(); expect(container.querySelector("pre")).toBeNull(); await userEvent.click(box); @@ -221,19 +222,19 @@ describe("ChatView — tool cards", () => { }); it("transitions pending → result → sealed without losing open state", async () => { const { rerender } = render(ChatView, { props: { chunks: shellChunks(null) } }); - expect(screen.getByLabelText("Running")).toHaveClass("loading-spinner"); + expect(screen.getByLabelText("Running")).toHaveClass("loading-dots", "loading-sm"); expect(screen.getByText("→ running…")).toHaveClass("tool-result", "text-base-content/60"); - const box = screen.getByRole("checkbox", { name: "Toggle tool call run_shell" }); + const box = screen.getByRole("checkbox", { name: "Toggle tool call Run command" }); await userEvent.click(box); expect(screen.getByText("running…")).toBeInTheDocument(); await rerender({ chunks: shellChunks("done") }); expect(screen.queryByLabelText("Running")).toBeNull(); expect(screen.queryByText("→ running…")).toBeNull(); expect(screen.getByLabelText("Succeeded")).toBeInTheDocument(); - expect(screen.getByRole("checkbox", { name: "Toggle tool call run_shell" })).toBe(box); + expect(screen.getByRole("checkbox", { name: "Toggle tool call Run command" })).toBe(box); expect(box).toBeChecked(); await rerender({ chunks: shellChunks("done", false, true) }); - expect(screen.getByRole("checkbox", { name: "Toggle tool call run_shell" })).toBe(box); + expect(screen.getByRole("checkbox", { name: "Toggle tool call Run command" })).toBe(box); expect(box).toBeChecked(); expect(screen.getByText("done")).toBeInTheDocument(); }); @@ -241,7 +242,7 @@ describe("ChatView — tool cards", () => { const { container } = render(ChatView, { props: { chunks: shellChunks("Permission denied", true) }, }); - const box = screen.getByRole("checkbox", { name: "Toggle tool call run_shell" }); + const box = screen.getByRole("checkbox", { name: "Toggle tool call Run command" }); expect(box).not.toBeChecked(); expect(screen.getByLabelText("Failed")).toHaveClass("text-error"); expect(screen.getByLabelText("Failed")).toHaveTextContent("✗"); @@ -257,7 +258,7 @@ describe("ChatView — tool cards", () => { expect(container.querySelectorAll(".chat-bubble")).toHaveLength(1); expect(screen.getByText("Call in earlier history")).toHaveClass("earlier-history"); expect(screen.getByLabelText("Succeeded")).toHaveClass("opacity-60"); - const box = screen.getByRole("checkbox", { name: "Toggle tool call run_shell" }); + const box = screen.getByRole("checkbox", { name: "Toggle tool call Run command" }); await userEvent.click(box); expect(screen.getByRole("note")).toHaveTextContent( "This call is earlier in the conversation. Show earlier messages to see it.", @@ -266,14 +267,14 @@ describe("ChatView — tool cards", () => { expect(screen.queryByRole("heading", { name: "Call" })).toBeNull(); expect(screen.getByText("contents")).toBeInTheDocument(); await rerender({ chunks }); - expect(screen.getByRole("checkbox", { name: "Toggle tool call run_shell" })).toBe(box); + expect(screen.getByRole("checkbox", { name: "Toggle tool call Run command" })).toBe(box); expect(box).toBeChecked(); expect(screen.getByText("$ bun run test")).toBeInTheDocument(); }); it("folds 13 output lines as 6 + toggle + 6, expands and collapses without closing the card", async () => { const output = Array.from({ length: 13 }, (_, i) => `output-${i + 1}`).join("\n"); const { rerender } = render(ChatView, { props: { chunks: shellChunks(output) } }); - const box = screen.getByRole("checkbox", { name: "Toggle tool call run_shell" }); + const box = screen.getByRole("checkbox", { name: "Toggle tool call Run command" }); await userEvent.click(box); const response = screen.getByRole("region", { name: "Response" }); expect(response).toHaveTextContent("output-1"); @@ -315,7 +316,7 @@ describe("ChatView — tool cards", () => { it("shows 12 output lines without an inner toggle", async () => { const output = Array.from({ length: 12 }, (_, i) => `output-${i + 1}`).join("\n"); render(ChatView, { props: { chunks: shellChunks(output) } }); - await userEvent.click(screen.getByRole("checkbox", { name: "Toggle tool call run_shell" })); + await userEvent.click(screen.getByRole("checkbox", { name: "Toggle tool call Run command" })); expect(screen.queryByRole("button", { name: /more lines|fewer lines/ })).toBeNull(); expect(screen.getByRole("region", { name: "Response" })).toHaveTextContent("output-12"); }); @@ -349,7 +350,7 @@ describe("ChatView — tool cards", () => { expect(screen.getByText("+2")).toHaveClass("text-success"); expect(screen.getByText("−1")).toHaveClass("text-error"); expect(screen.getByText("→ 1 replacement · diagnostics")).toBeInTheDocument(); - await userEvent.click(screen.getByRole("checkbox", { name: "Toggle tool call edit_file" })); + await userEvent.click(screen.getByRole("checkbox", { name: "Toggle tool call Edit file" })); // Unified diff: one tinted row per line, each with its own −/+ gutter marker. const diff = screen.getByRole("group", { name: "Edit diff" }); const removed = diff.querySelectorAll(".edit-diff-del"); @@ -383,7 +384,7 @@ it("renders compact, aligned generic fields with legible headings and inline sca ]; const { container } = render(ChatView, { props: { chunks } }); await userEvent.click( - screen.getByRole("checkbox", { name: "Toggle tool call mcp__github__create_issue" }), + screen.getByRole("checkbox", { name: "Toggle tool call Github: Create issue" }), ); for (const name of ["Call", "Response"]) { expect(screen.getByRole("heading", { name })).toHaveClass("font-semibold", "text-base-content"); @@ -427,7 +428,7 @@ it("removes a pending edit's diffstat when it fails, without hiding the attempte expect(screen.getByText("+1")).toBeInTheDocument(); expect(screen.getByText("−1")).toBeInTheDocument(); expect(screen.getByText("→ running…")).toBeInTheDocument(); - await userEvent.click(screen.getByRole("checkbox", { name: "Toggle tool call edit_file" })); + await userEvent.click(screen.getByRole("checkbox", { name: "Toggle tool call Edit file" })); await rerender({ chunks: [call, result] }); expect(screen.queryByText("+1")).toBeNull(); expect(screen.queryByText("−1")).toBeNull(); @@ -438,3 +439,34 @@ it("removes a pending edit's diffstat when it fails, without hiding the attempte expect(screen.getByText("bar")).toBeInTheDocument(); expect(screen.getByText("Error: oldString not found")).toBeInTheDocument(); }); + +it("shows an argument-less call without `{}` — empty header summary, 'No arguments' in the body", async () => { + const chunks: RenderedChunk[] = [ + { + seq: 1, + role: "assistant", + provisional: false, + chunk: { type: "tool-call", toolCallId: "m", toolName: "list_agent_models", input: {} }, + }, + { + seq: 2, + role: "tool", + provisional: false, + chunk: { + type: "tool-result", + toolCallId: "m", + toolName: "list_agent_models", + content: "Available named agents (3)", + isError: false, + }, + }, + ]; + const { container } = render(ChatView, { props: { chunks } }); + expect(screen.getByText("List agent models")).toBeInTheDocument(); + expect(container).not.toHaveTextContent("{}"); + await userEvent.click( + screen.getByRole("checkbox", { name: "Toggle tool call List agent models" }), + ); + expect(screen.getByText("No arguments")).toBeInTheDocument(); + expect(container).not.toHaveTextContent("{}"); +}); diff --git a/src/features/chat/ui/ToolCard.svelte b/src/features/chat/ui/ToolCard.svelte index 75c562d..c3da723 100644 --- a/src/features/chat/ui/ToolCard.svelte +++ b/src/features/chat/ui/ToolCard.svelte @@ -9,16 +9,18 @@ </script> <div class="tool-card"> - <LazyCollapse label={`Toggle tool call ${card.name}`} titleClass="pe-4"> + <LazyCollapse label={`Toggle tool call ${card.label}`} titleClass="pe-4"> {#snippet title()} <div class="tool-header" class:has-earlier-call={card.callDetail.kind === "unavailable"}> {#if card.status === "pending"} - <span class="loading loading-spinner loading-xs shrink-0" aria-label="Running"></span> + <!-- Same in-progress animation as the streaming thinking block. All three + status icons share a fixed w-5 slot so the row never shifts on settle. --> + <span class="loading loading-dots loading-sm w-5 shrink-0" aria-label="Running"></span> {:else if card.status === "error"} - <span class="shrink-0 text-error" aria-label="Failed">✗</span> + <span class="w-5 shrink-0 text-center text-error" aria-label="Failed">✗</span> {:else} <span - class="shrink-0 text-success" + class="w-5 shrink-0 text-center text-success" class:opacity-60={card.callDetail.kind === "unavailable"} aria-label="Succeeded">✓</span > @@ -38,9 +40,9 @@ d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" /> </svg> - <span class="tool-name truncate" title={card.name}>{card.name}</span> + <span class="tool-name truncate" title={card.name}>{card.label}</span> <span - class="tool-summary truncate text-base-content/90" + class="tool-summary truncate font-normal text-base-content/60" class:earlier-history={card.callDetail.kind === "unavailable"} title={card.callDetail.kind === "unavailable" ? card.callDetail.text : card.callSummary} > @@ -86,7 +88,9 @@ Call </h3> {/if} - {#if card.callDetail.kind === "command"} + {#if card.callDetail.kind === "none"} + <p class="italic text-base-content/60">No arguments</p> + {:else if card.callDetail.kind === "command"} <pre class="overflow-x-auto">$ {card.callDetail.text}</pre> {:else if card.callDetail.kind === "edit"} <div class="mb-2 break-all font-mono">{card.callDetail.path}</div> @@ -172,9 +176,11 @@ width: 100%; min-width: 0; } + /* English names are short phrases: keep them whole and let the call + summary absorb the squeeze. The cap only bites on long MCP names. */ .tool-name { - flex: 0 1 auto; - max-width: 30%; + flex: 0 0 auto; + max-width: 60%; } .tool-summary { flex: 1; @@ -257,9 +263,6 @@ .tool-badges { display: none; } - .tool-name { - max-width: 32%; - } .has-earlier-call .tool-result { display: none; } |
