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})`;
+}