web: pretty tool cards — args rendered as the real command/path (priority keys, label-less single field), output label; 276 tests green

This commit is contained in:
Raphael Westphal
2026-08-20 14:55:55 +02:00
parent 2d0839357d
commit bdfa61a8b2
4 changed files with 128 additions and 8 deletions
+56 -6
View File
@@ -162,6 +162,54 @@ function CopyButton({ text }: { text: string }): React.ReactNode {
);
}
// ---------- pretty tool args ----------
// Keys shown first when present (most readable "what is this tool doing"
// signal); the rest follow in their original order.
const ARG_PRIORITY: string[] = [
"command", "path", "file_path", "pattern", "url", "query", "content",
"prompt", "task", "description",
];
export interface ArgEntry {
k: string | null;
v: string;
}
/** Parse a tool's args (JSON object, JSON string, or raw text) into labeled
* display entries. A lone string entry renders without a label. */
export function toolArgsEntries(argsText: string): ArgEntry[] {
let parsed: unknown;
try {
parsed = JSON.parse(argsText);
} catch {
return [{ k: null, v: argsText }];
}
if (typeof parsed === "string")
return [{ k: null, v: parsed }];
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
return [{ k: null, v: argsText }];
const obj = parsed as Record<string, unknown>;
const keys = Object.keys(obj);
const ordered = [
...ARG_PRIORITY.filter((k) => k in obj),
...keys.filter((k) => !ARG_PRIORITY.includes(k)),
];
const entries: ArgEntry[] = [];
for (const k of ordered) {
const raw = obj[k];
const v =
typeof raw === "string"
? raw
: JSON.stringify(raw, null, 2) ?? String(raw);
entries.push({ k, v });
}
if (entries.length === 1 && entries[0] !== undefined)
return [{ k: null, v: entries[0].v }];
return entries;
}
function ToolCard({ tool }: { tool: ToolState }) {
const status: string = tool.running
? "running…"
@@ -180,14 +228,16 @@ function ToolCard({ tool }: { tool: ToolState }) {
</span>
</summary>
<div className="tool-body">
<div>
<strong>args</strong>
<pre style={{ margin: "4px 0 10px", whiteSpace: "pre-wrap" }}>
{tool.args}
</pre>
<div className="tool-args">
{toolArgsEntries(tool.args).map((e, i) => (
<div key={e.k ?? i} className="tool-arg">
{e.k !== null && <span className="arg-k">{e.k}</span>}
<pre className="arg-v">{e.v}</pre>
</div>
))}
</div>
<div>
<strong>result</strong>
<span className="arg-k">output</span>
<pre style={{ margin: "4px 0 0", whiteSpace: "pre-wrap" }}>
{tool.preview}
</pre>