diff --git a/ui/src/ui/chat-markdown.browser.test.ts b/ui/src/ui/chat-markdown.browser.test.ts index 17a898bac4c..a3499539f79 100644 --- a/ui/src/ui/chat-markdown.browser.test.ts +++ b/ui/src/ui/chat-markdown.browser.test.ts @@ -31,7 +31,42 @@ describe("chat markdown rendering", () => { await app.updateComplete; - const strong = app.querySelector(".sidebar-markdown strong"); - expect(strong?.textContent).toBe("world"); + const strongNodes = Array.from(app.querySelectorAll(".sidebar-markdown strong")); + expect(strongNodes.map((node) => node.textContent)).toContain("world"); + }); + + it("shows tool call request parameters in the sidebar", async () => { + const app = mountApp("/chat"); + await app.updateComplete; + + const timestamp = Date.now(); + app.chatMessages = [ + { + role: "assistant", + content: [ + { + type: "toolcall", + name: "sessions_spawn", + arguments: { agentId: "research", prompt: "hello" }, + }, + ], + timestamp, + }, + ]; + + await app.updateComplete; + + const toolCard = app.querySelector(".chat-tool-card"); + expect(toolCard).not.toBeNull(); + toolCard?.click(); + + await app.updateComplete; + + const sidebar = app.querySelector(".sidebar-markdown"); + expect(sidebar?.textContent).toContain("Arguments"); + expect(sidebar?.textContent).toContain("agentId"); + expect(sidebar?.textContent).toContain("research"); + expect(sidebar?.textContent).toContain("prompt"); + expect(sidebar?.textContent).toContain("hello"); }); }); diff --git a/ui/src/ui/chat/tool-cards.ts b/ui/src/ui/chat/tool-cards.ts index acd427b9e77..d5975a16102 100644 --- a/ui/src/ui/chat/tool-cards.ts +++ b/ui/src/ui/chat/tool-cards.ts @@ -5,7 +5,7 @@ import type { ToolCard } from "../types/chat-types.ts"; import { TOOL_INLINE_THRESHOLD } from "./constants.ts"; import { extractTextCached } from "./message-extract.ts"; import { isToolResultMessage } from "./message-normalizer.ts"; -import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers.ts"; +import { buildToolSidebarContent, getTruncatedPreview } from "./tool-helpers.ts"; export function extractToolCards(message: unknown): ToolCard[] { const m = message as Record; @@ -56,14 +56,14 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content: const canClick = Boolean(onOpenSidebar); const handleClick = canClick ? () => { - if (hasText) { - onOpenSidebar!(formatToolOutputForSidebar(card.text!)); - return; - } - const info = `## ${display.label}\n\n${ - detail ? `**Command:** \`${detail}\`\n\n` : "" - }*No output — tool completed successfully.*`; - onOpenSidebar!(info); + onOpenSidebar!( + buildToolSidebarContent({ + title: display.label, + detail, + args: card.args, + output: card.text, + }), + ); } : undefined; @@ -95,11 +95,7 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content: ${icons[display.icon]} ${display.label} - ${ - canClick - ? html`${hasText ? "View" : ""} ${icons.check}` - : nothing - } + ${canClick ? html`View ${icons.check}` : nothing} ${isEmpty && !canClick ? html`${icons.check}` : nothing} ${detail ? html`
${detail}
` : nothing} diff --git a/ui/src/ui/chat/tool-helpers.test.ts b/ui/src/ui/chat/tool-helpers.test.ts index f18cd738a7f..18fdc0d72a4 100644 --- a/ui/src/ui/chat/tool-helpers.test.ts +++ b/ui/src/ui/chat/tool-helpers.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from "vitest"; -import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers.ts"; +import { + buildToolSidebarContent, + formatToolOutputForSidebar, + formatToolPayloadForSidebar, + getTruncatedPreview, +} from "./tool-helpers.ts"; describe("tool-helpers", () => { describe("formatToolOutputForSidebar", () => { @@ -138,4 +143,54 @@ describe("tool-helpers", () => { expect(result.endsWith("…")).toBe(true); }); }); + + describe("formatToolPayloadForSidebar", () => { + it("formats structured arguments as JSON", () => { + const result = formatToolPayloadForSidebar({ agentId: "research", count: 2 }); + + expect(result).toBe(`\`\`\`json +{ + "agentId": "research", + "count": 2 +} +\`\`\``); + }); + + it("returns null for undefined arguments", () => { + expect(formatToolPayloadForSidebar(undefined)).toBeNull(); + }); + + it("returns null for null arguments", () => { + expect(formatToolPayloadForSidebar(null)).toBeNull(); + }); + }); + + describe("buildToolSidebarContent", () => { + it("includes arguments and output when both are present", () => { + const result = buildToolSidebarContent({ + title: "Session Spawn", + detail: "sessions_spawn research", + args: { agentId: "research" }, + output: '{"status":"ok"}', + }); + + expect(result).toContain("## Session Spawn"); + expect(result).toContain("**Command:** `sessions_spawn research`"); + expect(result).toContain("**Arguments**"); + expect(result).toContain('"agentId": "research"'); + expect(result).toContain("**Output**"); + expect(result).toContain('"status": "ok"'); + }); + + it("shows completion text when output is absent", () => { + const result = buildToolSidebarContent({ + title: "Session Spawn", + args: { agentId: "research" }, + }); + + expect(result).toContain("**Arguments**"); + expect(result).toContain('"agentId": "research"'); + expect(result).toContain("*No output - tool completed successfully.*"); + }); + }); }); diff --git a/ui/src/ui/chat/tool-helpers.ts b/ui/src/ui/chat/tool-helpers.ts index 322b6058f6a..0ceb6351c6b 100644 --- a/ui/src/ui/chat/tool-helpers.ts +++ b/ui/src/ui/chat/tool-helpers.ts @@ -22,6 +22,46 @@ export function formatToolOutputForSidebar(text: string): string { return text; } +/** + * Format tool-call input/arguments for display in the sidebar. + * Uses JSON code blocks for structured values and preserves plain text input. + */ +export function formatToolPayloadForSidebar(value: unknown): string | null { + if (value === undefined || value === null) { + return null; + } + if (typeof value === "string") { + return formatToolOutputForSidebar(value); + } + try { + return "```json\n" + JSON.stringify(value, null, 2) + "\n```"; + } catch { + return String(value); + } +} + +export function buildToolSidebarContent(params: { + title: string; + detail?: string; + args?: unknown; + output?: string; +}): string { + const sections = [`## ${params.title}`]; + if (params.detail) { + sections.push(`**Command:** \`${params.detail}\``); + } + const args = formatToolPayloadForSidebar(params.args); + if (args !== null) { + sections.push(`**Arguments**\n${args}`); + } + if (typeof params.output === "string") { + sections.push(`**Output**\n${formatToolOutputForSidebar(params.output)}`); + } else { + sections.push("*No output - tool completed successfully.*"); + } + return sections.join("\n\n"); +} + /** * Get a truncated preview of tool output text. * Truncates to first N lines or first N characters, whichever is shorter.