diff --git a/gadget-code/frontend/src/components/ChatTurn.tsx b/gadget-code/frontend/src/components/ChatTurn.tsx index f165ee8..79cfcfd 100644 --- a/gadget-code/frontend/src/components/ChatTurn.tsx +++ b/gadget-code/frontend/src/components/ChatTurn.tsx @@ -1,6 +1,7 @@ import { memo, useState, useCallback } from "react"; import { marked } from "marked"; import type { ChatTurn as ChatTurnType, ChatTurnBlockTool } from "../lib/api"; +import ToolCallDisplay from "./ToolCallDisplay"; interface ChatTurnProps { turn: ChatTurnType; @@ -151,16 +152,13 @@ const ChatTurn = memo(function ChatTurn({ turn }: ChatTurnProps) { const subagent = toolCall.subagent; return (
-
- - {toolCall.name} - {toolCall.response && !subagent && ( - - )} - {subagent && subagent.stats && ( - - )} -
+ {subagent && }
); @@ -238,14 +236,14 @@ function SubagentDisplay({
{subagent.toolCalls.map((tc, i) => ( -
- - {tc.name} - {tc.response && } -
+ callId={tc.callId} + name={tc.name} + parameters={tc.parameters} + response={tc.response} + defaultExpanded={false} + /> ))}
diff --git a/gadget-code/frontend/src/components/ToolCallDisplay.tsx b/gadget-code/frontend/src/components/ToolCallDisplay.tsx new file mode 100644 index 0000000..c38e9ea --- /dev/null +++ b/gadget-code/frontend/src/components/ToolCallDisplay.tsx @@ -0,0 +1,130 @@ +import { useState, useCallback } from "react"; +import { marked } from "marked"; +import { formatToolCallSignature } from "../lib/toolCallFormat"; + +// Configure marked with breaks enabled (consistent with ChatTurn) +marked.use({ breaks: true }); + +interface ToolCallDisplayProps { + callId: string; + name: string; + parameters?: string; + response?: string; + createdAt?: string; + defaultExpanded?: boolean; +} + +/** + * Renders a single tool call with expand/collapse behavior. + * + * Collapsed: ▸ toolName(param: value, ...) + * Expanded: shows response (raw/markdown toggle) + metadata + */ +export default function ToolCallDisplay({ + callId, + name, + parameters, + response, + createdAt, + defaultExpanded = false, +}: ToolCallDisplayProps) { + const [expanded, setExpanded] = useState(defaultExpanded); + const [renderMarkdown, setRenderMarkdown] = useState(false); + const toggle = useCallback(() => setExpanded((p) => !p), []); + + const signature = formatToolCallSignature(name, parameters); + const hasResponse = !!response; + + return ( +
+ {/* Header row — always visible, clickable to toggle */} + + + {/* Expanded detail panel */} + {expanded && ( +
+ {/* Metadata row */} +
+ id: {callId} + {createdAt && ( + {new Date(createdAt).toLocaleTimeString()} + )} +
+ + {/* Response block */} + {hasResponse && ( +
+ {/* Response header with RAW/MD toggle */} +
+ Response +
+ + +
+
+ + {/* Response content */} + {renderMarkdown ? ( +
+ ) : ( +
+ {response} +
+ )} +
+ )} + + {/* No response indicator */} + {!hasResponse && ( +
+ No response +
+ )} +
+ )} +
+ ); +} diff --git a/gadget-code/frontend/src/index.css b/gadget-code/frontend/src/index.css index 106947c..bb606ba 100644 --- a/gadget-code/frontend/src/index.css +++ b/gadget-code/frontend/src/index.css @@ -56,14 +56,44 @@ button { } .tool-call { - margin-top: theme("spacing.4"); - margin-bottom: theme("spacing.4"); + margin-top: 0.25rem; + margin-bottom: 0.25rem; } .tool-call + .tool-call { margin-top: 0; } +/* Tool call detail panel (expanded) */ +.tool-call-detail { + border-left: 2px solid var(--color-border-default); + padding-left: 0.5rem; +} + +/* Raw response display — fixed-width, scrollable */ +.tool-call-response-raw { + font-family: var(--font-mono); + font-size: 0.8em; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; + overflow-y: auto; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border-default); + border-radius: 4px; + padding: 0.5rem 0.7rem; +} + +/* Markdown-rendered response display — scrollable */ +.tool-call-response-md { + max-height: 400px; + overflow-y: auto; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border-default); + border-radius: 4px; + padding: 0.5rem 0.7rem; +} + input, textarea { font-family: inherit; diff --git a/gadget-code/frontend/src/lib/toolCallFormat.ts b/gadget-code/frontend/src/lib/toolCallFormat.ts new file mode 100644 index 0000000..988b701 --- /dev/null +++ b/gadget-code/frontend/src/lib/toolCallFormat.ts @@ -0,0 +1,82 @@ +/** + * Tool call formatting utilities. + * + * Formats tool call parameters as function-call signature syntax: + * toolName(param1: "value", param2: 42, param3: {...}) + */ + +/** + * Truncate a string to maxLength, appending "..." if truncated. + */ +export function truncate(value: string, maxLength: number): string { + if (value.length <= maxLength) return value; + return value.slice(0, maxLength - 3) + "..."; +} + +/** + * Format a single parameter value for display in a function-call signature. + * + * - strings: quoted with truncation at 120 chars + * - numbers/booleans/null: bare + * - objects: "{...}" truncated + * - arrays: "[...]" truncated + */ +export function formatParamValue(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "boolean") return String(value); + if (typeof value === "number") return String(value); + if (typeof value === "string") { + const truncated = truncate(value, 120); + // Only quote if it contains spaces or special chars, or if truncated + // Always quote for consistency — function-call syntax expects it + return `"${truncated}"`; + } + if (Array.isArray(value)) { + return "[...]"; + } + if (typeof value === "object") { + return "{...}"; + } + return String(value); +} + +/** + * Parse a JSON parameters string and format it as a function-call signature. + * + * @example + * formatToolCallSignature("file_read", '{"path": "src/foo.ts", "recursive": true}') + * // → 'file_read(path: "src/foo.ts", recursive: true)' + * + * @example + * formatToolCallSignature("list_files", undefined) + * // → 'list_files()' + */ +export function formatToolCallSignature( + name: string, + parameters?: string +): string { + if (!parameters) return `${name}()`; + + let parsed: Record; + try { + parsed = JSON.parse(parameters); + } catch { + // If it's not valid JSON, show it as a raw string argument + return `${name}(${truncate(parameters, 200)})`; + } + + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + // Non-object JSON — show as single argument + return `${name}(${formatParamValue(parsed)})`; + } + + const entries = Object.entries(parsed); + if (entries.length === 0) return `${name}()`; + + const params = entries + .map(([key, val]) => `${key}: ${formatParamValue(val)}`) + .join(", "); + + return `${name}(${params})`; +}