Problem
spawn_subagent returns its child's final answer wrapped in a JSON envelope via formatSubagentResult (src/tools/subagent.ts:337):
return JSON.stringify({ success: true, output: r.output, turns, tool_iters, elapsed_ms, cost_usd });
That string becomes the tool result, which the TUI renders through ToolCard (src/cli/ui/cards/ToolCard.tsx). The card body just splits on \n and dumps the tail through plain <Text> (ToolCard.tsx:70). No markdown pass, no JSON unwrap.
In practice the child loop is an LLM — its assistant_final is markdown by default: ## Headings, **bold**, bullet lists, fenced code blocks. Because we (a) JSON-stringify it and (b) render the JSON body as raw text, the user sees:
{"success":true,"output":"## Findings\n\n- \`src/foo.ts:42\` …\n\n\`\`\`ts\nfoo()\n\`\`\`\n", …}
— literal \n, literal backticks, literal **. The streaming reply path uses Markdown (StreamingCard.tsx:83); the subagent path doesn't.
What I want
When the tool name is spawn_subagent (and the result is a successful envelope), the card body should render output through the same Markdown component the streaming reply uses, not through raw Text. Failure envelopes can stay as-is — the error field is a single line.
Sketch:
- In
ToolCard, branch on card.name === "spawn_subagent". Parse the JSON; on a success: true shape with a string output, render <Markdown text={output} /> instead of the line-tail loop.
- Keep the existing tail/clip behavior as a fallback for parse failures and for
success: false.
Out of scope
- Changing what
formatSubagentResult returns. The model still wants the JSON envelope so it can read success, cost_usd, etc.; only the TUI projection needs to change.
- Markdown rendering for every tool result. Most tools (
read_file, run_command, …) emit code/log output where literal ** is correct.
Problem
spawn_subagentreturns its child's final answer wrapped in a JSON envelope viaformatSubagentResult(src/tools/subagent.ts:337):That string becomes the tool result, which the TUI renders through
ToolCard(src/cli/ui/cards/ToolCard.tsx). The card body just splits on\nand dumps the tail through plain<Text>(ToolCard.tsx:70). No markdown pass, no JSON unwrap.In practice the child loop is an LLM — its
assistant_finalis markdown by default:## Headings,**bold**, bullet lists, fenced code blocks. Because we (a) JSON-stringify it and (b) render the JSON body as raw text, the user sees:— literal
\n, literal backticks, literal**. The streaming reply path usesMarkdown(StreamingCard.tsx:83); the subagent path doesn't.What I want
When the tool name is
spawn_subagent(and the result is a successful envelope), the card body should renderoutputthrough the sameMarkdowncomponent the streaming reply uses, not through rawText. Failure envelopes can stay as-is — theerrorfield is a single line.Sketch:
ToolCard, branch oncard.name === "spawn_subagent". Parse the JSON; on asuccess: trueshape with a stringoutput, render<Markdown text={output} />instead of the line-tail loop.success: false.Out of scope
formatSubagentResultreturns. The model still wants the JSON envelope so it can readsuccess,cost_usd, etc.; only the TUI projection needs to change.read_file,run_command, …) emit code/log output where literal**is correct.