Agent chat: tool results show their content, not the JSON envelope around it
Leo-ask: ask-16f0b7197f
Leo: "Bro, like, but can you just, like that JSON, it's so stupid. I mean, why is it wrapping it with that? Like, don't we have more rendering?"
An opened tool call in the agent transcript showed what the harness handed the pane, and that is often the protocol's JSON envelope rather than the result. Now the opened call shows the result itself.
What changes
New conversation/toolResult.ts, used by the generic tool row (ToolRow.tsx) and Find in Chat (chatFind.ts).
Envelopes unwrap to their text when the text is all they say:
- Lists of MCP and ACP text blocks (
[{"type":"text","text":"…"}]), including ACP{type:"content"}wrappers. A block must have only its own fields, so a tool's own JSON that names a block type (Notion rich text) stays JSON. - An MCP
CallToolResultwith nostructuredContent. {"result": …, "error": null}whose result is a string or another envelope, and an error that is a string or only{code, message}.- Codex function output:
{"output":"…","metadata":{…}}. - Envelopes inside envelopes (bounded depth), and JSON text inside text.
Nothing is hidden. An envelope that says more than its text stays JSON, as before. That covers images, resources, links, audio, structured content, error data or extra fields, and empty envelopes. The edge-case handling for those is the follow-up PR (branch tp/unwrap-tool-json-edges).
The text renders as Markdown when it looks like Markdown: a heading, list, fence, table or quote. Fences stay plain code at any depth, so tool text never draws a diagram or a render card. Any other text renders as plain text.
Other JSON that arrives on one line is pretty-printed into the folding output card, never one long raw line. Some JSON stays exactly as printed:
- JSON printed on lines, such as a JSON file a tool read (directly or through an MCP tool), even when it looks like an envelope;
- JSON holding a number a double would change (
9007199254740993,10.000000000000001); - JSON nested past 64 levels.
Shell output is untouched.
Find in Chat reads what the row draws: the unwrapped text, with Markdown read without its marks.
SendMessage and Agent results are left to tp/sent-card-no-json.
Before / after
Gallery entry agent-pane.transcript, variant tool-result-envelopes, from synthetic fixtures. The before is this branch's base with the same fixtures; the after is 1687f9d. Captured on the Linux gallery (Chromium), with the live run's scroll viewport lifted so all five opened calls fit.
| Before | After |
|---|---|
![]() | ![]() |
![]() | ![]() |
From top to bottom:
- TaskStop's one-line JSON is pretty-printed.
- The MCP Markdown list renders as Markdown.
- The review gate's
{error, result:{content}}reads as its sentence. - Codex's
{output, metadata}reads as its output. - package.json stays exactly as read.
Tests
webviews/src/agent-session/acpmux/conversation/toolResult.test.ts and toolResultMarkdown.test.tsx cover:
- each envelope shape unwrapping;
- envelopes that say more than their text staying JSON;
- lookalike JSON and JSON files staying as they are;
- numbers and deep nesting;
- injected context;
- Find matching what is drawn;
- plain fences in nested blocks.
Ran on huge-blue: bun test src/agent-session/acpmux/conversation/ src/agent-session/acpmux/toolImages test/chat-find-matches.test.ts test/agent-pane-mcp-tool-rows.test.tsx, 180 pass. Also ran oxlint with the React plugin on the changed files. tsc shows the same 33 errors as base, none in these files.
Review
- Ten Codex xhigh reviews of the larger version (up to e2c9706, 998 lines) each found edge cases in the areas now left as JSON. Each review confirmed the previous fixes.
- That version was split. This PR keeps the core every review passed, and the edge-case handling moves to the follow-up.
- Codex xhigh review of this head: pending.
Changelog
- Fixed: Opened tool calls in the agent chat show their result, not the JSON envelope around it; other JSON is pretty-printed.
Agent session: cosmic-gray
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Opened tool calls in the agent chat now show the tool's actual result instead of the JSON envelope the harness wrapped it around.
- New
toolResult.tsunwraps MCP and ACP content lists,CallToolResultobjects,{result, error}envelopes, and Codex's{output, metadata}output; an error shows its message, and data besides it stays JSON. Only real content blocks unwrap, so JSON that merely names a block type (Notion rich text, Slack images) stays JSON, and an envelope holding anything beyond its text, or nothing, stays JSON. - Result text renders as Markdown when it is Markdown, with fences kept as code at any depth inside quotes and list items; other JSON is pretty-printed, folds after eight lines, and strings that hold JSON stay strings.
- JSON a tool returned on lines (a file it read), numbers a double would round, and JSON nested past 64 levels stay as printed; shell output is untouched.
- Host-injected context is dropped from unwrapped text, and Find in Chat reads the same unwrapped text the row draws; a closed row reads once per output, so re-renders parse nothing.
- New tests pin that MCP calls draw in MCP, message or sub-session cards, never as ToolRows, and that fences stay plain code at any nesting depth.



