From e488fe0509cd4e9f3f5216e52a2f6d608009bc7d Mon Sep 17 00:00:00 2001 From: Adam Firestone Date: Mon, 10 Aug 2026 16:58:46 -0500 Subject: [PATCH] fix(web): format tool-call JSON previews as code - Split labeled JSON payloads from tool-call summaries - Render JSON previews with monospace styling while preserving plain text --- .../components/chat/MessagesTimeline.test.tsx | 30 ++++++++++++++++++- .../src/components/chat/MessagesTimeline.tsx | 25 +++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 4c4b810f93ab..9a60ff8e25cb 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -144,6 +144,7 @@ function matchMedia() { let MessagesTimeline: typeof import("./MessagesTimeline").MessagesTimeline; let buildToolCallExpandedBody: typeof import("./MessagesTimeline").buildToolCallExpandedBody; let WorkEntryExpandedBody: typeof import("./MessagesTimeline").WorkEntryExpandedBody; +let splitPreviewJsonPayload: typeof import("./MessagesTimeline").splitPreviewJsonPayload; beforeAll(async () => { const classList = { @@ -177,7 +178,7 @@ beforeAll(async () => { }, }); - ({ MessagesTimeline, WorkEntryExpandedBody, buildToolCallExpandedBody } = + ({ MessagesTimeline, WorkEntryExpandedBody, buildToolCallExpandedBody, splitPreviewJsonPayload } = await import("./MessagesTimeline")); }, 30_000); @@ -236,6 +237,33 @@ function buildUserTimelineEntry(text: string) { }; } +describe("splitPreviewJsonPayload", () => { + it("splits labeled tool-call summaries into label and JSON payload", () => { + expect(splitPreviewJsonPayload('Skill: {"skill":"codex-first","args":"Implement"}')).toEqual({ + label: "Skill:", + payload: '{"skill":"codex-first","args":"Implement"}', + }); + }); + + it("accepts payloads truncated mid-JSON and bare payloads without a label", () => { + expect(splitPreviewJsonPayload('ToolSearch: {"query":"select:Re...')).toEqual({ + label: "ToolSearch:", + payload: '{"query":"select:Re...', + }); + expect(splitPreviewJsonPayload('{"query":"x"}')).toEqual({ + label: null, + payload: '{"query":"x"}', + }); + expect(splitPreviewJsonPayload('["a","b"]')).toEqual({ label: null, payload: '["a","b"]' }); + }); + + it("leaves plain-text previews untouched", () => { + expect(splitPreviewJsonPayload("Read src/index.ts")).toBeNull(); + expect(splitPreviewJsonPayload("Note: {braces} in prose")).toBeNull(); + expect(splitPreviewJsonPayload("See: [link](https://example.com)")).toBeNull(); + }); +}); + describe("MessagesTimeline", () => { it("extracts full provider tool output for expanded work rows", () => { const makeEntry = (toolData: unknown) => ({ diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 3af949baea4d..a3b67f9fd447 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -2050,6 +2050,21 @@ function workEntryDisplayText(workEntry: TimelineWorkEntry, workspaceRoot: strin }; } +// Dynamic tool-call summaries arrive as "ToolName: {…json…}" (see the provider +// adapters' summarizeToolRequest); the payload may be truncated mid-JSON, so +// detect it by shape rather than parsing. +export function splitPreviewJsonPayload( + preview: string, +): { label: string | null; payload: string } | null { + const separator = preview.indexOf(": "); + const label = separator > 0 ? preview.slice(0, separator + 1) : null; + const payload = separator > 0 ? preview.slice(separator + 2) : preview; + if (!/^(?:\{["}]|\[["{\]])/.test(payload)) { + return null; + } + return { label, payload }; +} + function workEntryRawCommand( workEntry: Pick, ): string | null { @@ -2503,6 +2518,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { const entryIconName = showWarningIndicator ? "x" : workEntryIconName(workEntry); const { heading, preview, displayText } = workEntryDisplayText(workEntry, workspaceRoot); const previewIsCommand = preview !== null && preview === workEntry.command; + const previewJson = !previewIsCommand && preview ? splitPreviewJsonPayload(preview) : null; const expandedBody = useMemo( () => buildToolCallExpandedBody(workEntry, workspaceRoot), [workEntry, workspaceRoot], @@ -2596,7 +2612,14 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { : "work-entry-preview-clip overflow-hidden whitespace-nowrap", )} > - {preview} + {previewJson ? ( + <> + {previewJson.label ? <>{previewJson.label} : null} + {previewJson.payload} + + ) : ( + preview + )} )}