feat(web): add a raw view to tool call cards #227

Merged
lucas merged 1 commit from agent/issue-226-raw-tool-view into main 2026-11-06 11:55:13 +01:00
Collaborator

Closes: #226

A braces button in every tool call card header swaps the per-tool renderer for the raw arguments and raw result.

What's in it

  • apps/web/src/lib/raw-json.ts — formatRawJson(value): JSON.stringify(v, null, 2) with two deviations.
    • Multi-line strings print real line breaks; continuation lines are padded to the column of that value's own opening quote, so the block hangs under the value. Only \n is converted — \t, \r, \", \\ and control characters keep their JSON escapes, so payload text can't be confused with surrounding punctuation.
    • type: "image" objects with a string data get the data replaced by ‹image data elided: N chars›; siblings (mimeType, …) are untouched. Long bash/read text is rendered in full.
    • Consequently the output is deliberately not valid JSON.
  • RawToolView: two labelled sections, args and result (or partial while only a partial result exists; a plain "no result yet" / "no stream data" line when neither). Mounted outside ToolRenderBoundary, so raw still works when a renderer throws, and fed the outer args/stream so write → xd://… dispatches show the call that actually happened rather than the inner reconstruction.
  • ToolCallCard: the header row became a wrapper div holding the existing expand button (unchanged children; hover highlight moved to the wrapper so it still covers the row) plus the toggle button — a nested <button> would be invalid HTML. The toggle is rendered on collapsed cards too and opens the card, so one press goes from collapsed to raw. State is a local useState, per card, ephemeral.
  • CodeBlock gained an optional copyText?: string | (() => string), passed to the CopyButton on both branches. The raw view uses it so the screen shows the readable, elided render while the clipboard gets complete, valid, unelided JSON — full base64 included. All existing callers omit it and are unaffected.

Verification

  • bun run typecheck (all workspaces) and bun --cwd apps/web run build: clean.
  • bun test (full suite): 1537 pass, 0 fail — including the new apps/web/src/lib/raw-json.test.ts (byte-identical output to JSON.stringify for newline-free values, the worked 15-space continuation example, array-element indentation, escape preservation, image elision and its non-elision on type: "text", undefined → "", null → "null").
  • Highlighting was the one open question in the plan: checked by running hljs.highlight(code, { language: "json", ignoreIllegals: true }) over the formatter's output for a multi-line payload. The broken string is highlighted as a single hljs-string span across the line breaks and the rest of the document keeps its normal token classes, so language="json" is kept.
  • Not done: the manual in-browser pass from the plan (live session, braces button on bash / forgejo_* / xd:// / screenshot calls, clipboard paste). This session has no browser; the behaviour above is argued from the code and the unit tests, and the visual check is worth a minute during review.

Notes

No server or protocol change — the data was already complete client-side.

Closes: #226 A braces button in every tool call card header swaps the per-tool renderer for the raw arguments and raw result. ## What's in it - **`apps/web/src/lib/raw-json.ts` — `formatRawJson(value)`**: `JSON.stringify(v, null, 2)` with two deviations. - Multi-line strings print real line breaks; continuation lines are padded to the column of that value's own opening quote, so the block hangs under the value. Only `\n` is converted — `\t`, `\r`, `\"`, `\\` and control characters keep their JSON escapes, so payload text can't be confused with surrounding punctuation. - `type: "image"` objects with a string `data` get the data replaced by `‹image data elided: N chars›`; siblings (`mimeType`, …) are untouched. Long `bash`/`read` text is rendered in full. - Consequently the output is deliberately not valid JSON. - **`RawToolView`**: two labelled sections, `args` and `result` (or `partial` while only a partial result exists; a plain "no result yet" / "no stream data" line when neither). Mounted *outside* `ToolRenderBoundary`, so raw still works when a renderer throws, and fed the **outer** `args`/`stream` so `write` → `xd://…` dispatches show the call that actually happened rather than the inner reconstruction. - **`ToolCallCard`**: the header row became a wrapper `div` holding the existing expand button (unchanged children; hover highlight moved to the wrapper so it still covers the row) plus the toggle button — a nested `<button>` would be invalid HTML. The toggle is rendered on collapsed cards too and opens the card, so one press goes from collapsed to raw. State is a local `useState`, per card, ephemeral. - **`CodeBlock`** gained an optional `copyText?: string | (() => string)`, passed to the `CopyButton` on both branches. The raw view uses it so the screen shows the readable, elided render while the clipboard gets complete, valid, unelided JSON — full base64 included. All existing callers omit it and are unaffected. ## Verification - `bun run typecheck` (all workspaces) and `bun --cwd apps/web run build`: clean. - `bun test` (full suite): 1537 pass, 0 fail — including the new `apps/web/src/lib/raw-json.test.ts` (byte-identical output to `JSON.stringify` for newline-free values, the worked 15-space continuation example, array-element indentation, escape preservation, image elision and its non-elision on `type: "text"`, `undefined` → `""`, `null` → `"null"`). - Highlighting was the one open question in the plan: checked by running `hljs.highlight(code, { language: "json", ignoreIllegals: true })` over the formatter's output for a multi-line payload. The broken string is highlighted as a single `hljs-string` span across the line breaks and the rest of the document keeps its normal token classes, so `language="json"` is kept. - Not done: the manual in-browser pass from the plan (live session, braces button on `bash` / `forgejo_*` / `xd://` / screenshot calls, clipboard paste). This session has no browser; the behaviour above is argued from the code and the unit tests, and the visual check is worth a minute during review. ## Notes No server or protocol change — the data was already complete client-side.
A per-tool renderer is the only way to see a tool call today, and every
one of them shapes the payload: result details are dropped whenever any
text block exists, header summaries truncate, and a renderer that throws
leaves nothing at all. The braces button in a card header now swaps the
renderer for the arguments and result as they arrived, mounted outside
the render boundary so it survives a broken renderer, and showing the
outer call for xd:// write dispatches rather than the reconstruction the
inner renderer gets.

The payload is rendered by a formatter that breaks multi-line strings
into real lines hanging under their opening quote and replaces image
base64 with a placeholder naming its length, so shell output and
screenshots stay scannable. That output is intentionally not valid JSON,
so code blocks can now carry separate clipboard text and the Copy button
hands over exact, unelided JSON.

Co-Authored-By: Claude Opus 5 via omp-deck
omp-deck force-pushed agent/issue-226-raw-tool-view from 70037548dc to c1c287d773 2026-11-06 11:48:46 +01:00 Compare
lucas approved these changes 2026-11-06 11:55:08 +01:00
lucas merged commit 853004cce6 into main 2026-11-06 11:55:13 +01:00
lucas deleted branch agent/issue-226-raw-tool-view 2026-11-06 11:55:13 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/omp-deck!227
No description provided.