diff options
| author | Adam Malczewski <[email protected]> | 2026-10-15 09:19:43 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-15 09:19:43 +0900 |
| commit | 633b94326ec4b33fe19d65d02ef78d8a6b14e4e9 (patch) | |
| tree | da640b98bf65fba2cc5c1e96354b990ef7c04a63 /src/features/chat/ui | |
| parent | ead9530e661a48050988450b6a70d2240f2ea8b1 (diff) | |
| download | dispatch-web-633b94326ec4b33fe19d65d02ef78d8a6b14e4e9.tar.gz dispatch-web-633b94326ec4b33fe19d65d02ef78d8a6b14e4e9.zip | |
feat(tool-call-cards): English tool names, no '{}' for argument-less calls, thinking-style dots while running
Diffstat (limited to 'src/features/chat/ui')
| -rw-r--r-- | src/features/chat/ui/ChatView.test.ts | 66 | ||||
| -rw-r--r-- | src/features/chat/ui/ToolCard.svelte | 27 |
2 files changed, 64 insertions, 29 deletions
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; } |
