A way to view raw tool call data #226
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#226
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Every tool call in the chat transcript is drawn by a per-tool renderer that
shapes the payload for readability. That shaping is lossy:
extractResultText(
apps/web/src/components/tools/shared.tsx) keeps onlytype: "text"contentblocks and drops
result.detailsentirely whenever any text exists, andsummarizeArgstruncates header values to 60–80 characters. When a rendererlooks wrong, shows nothing, or throws, there is no way to see what the tool
actually received and returned.
The data is already fully client-side — no server or protocol work is needed.
ws.tsforwardssession_eventframes verbatim,in-process.ts snapshot()returns the SDK message array unmodified, and the reducer stores
args,result,partialResult,isErrorandstatusonToolCallStream(
apps/web/src/lib/types.ts:146-156). Nothing between the agent and therenderer truncates or redacts.
Wanted: a per-card toggle that swaps the nice renderer for the raw arguments
and raw result, readable enough to actually scan, and copyable as exact JSON.
Plan
1. New pure formatter
formatRawJsonCreate
apps/web/src/lib/raw-json.tsexporting:No equivalent exists —
JSON.stringify(v, null, 2)is used directly inGeneric.tsx:11,ToolPermissionDialog.tsx:82, andcanvas/StepInspector.tsx,and none of them render embedded newlines.
It emits pretty-printed JSON with two deviations from
JSON.stringify(v, null, 2):a) Multi-line strings render their line breaks. A string value containing
\nis emitted with real line breaks. Continuation lines are left-padded withspaces to the column at which that value's own opening
"sits, so the blockhangs under the value. Example — input
{"content":[{"type":"text","text":"total 12\ndrwxr-xr-x 3 lucas\n"}],"isError":false}produces exactly:
(
"text"starts at column 6; its value's opening quote is at column 15; everycontinuation line gets 15 spaces.) For a string in an array position the same
rule applies against that element's own opening quote. Only
\nis converted;\t,\"and\\stay escaped so payload text can never be mistaken forsurrounding JSON punctuation. A
\r\nin the payload emits the\rescapedand breaks on the
\n.Consequence to accept, not work around: the output is deliberately not valid
JSON. Copying is handled separately in step 3.
b) Image data is elided. When serializing an object with
type === "image"and a string
dataproperty (theImageBlockshape,lib/types.ts:32-36;produced by
generate_image,browser, andreadon raster files), thedatavalue is replaced by the literal placeholder string
with the real character count substituted.
mimeTypeand all sibling keys areuntouched. Only
type: "image"objects are elided; longbashorreadtextis rendered in full — that is the point of a raw view.
CodeBlockalreadydegrades on oversized input (
MAX_HIGHLIGHT_BYTES = 256 * 1024, then a bannerplus unhighlighted
<pre>), andmax-h-72 overflow-autocaps the height.Other cases:
undefinedreturns the empty string;nullreturns"null";non-finite numbers serialize as
null(matchingJSON.stringify). Circularreferences cannot occur — every value originated as JSON over the WebSocket —
so no cycle guard is added.
2. Raw view in
ToolCallCardAll changes in
apps/web/src/components/tools/ToolCallCard.tsx.Header restructure. The header is currently one full-width
<button>(lines ~89-120). A nested
<button>is invalid HTML, so wrap it:<div className="flex w-full items-center gap-2 rounded-md px-1.5 py-1 hover:bg-paper-3/60">.<button>keeps every child it has today (chevron, status dot,name, summary, eval badge, the
ml-autostatus/duration group) and becomesclassName="flex min-w-0 flex-1 items-center gap-2 text-left font-mono text-xs";the
hover:bg-paper-3/60and padding move to the wrapper so the hoverhighlight still covers the whole row.
Import
Bracesfromlucide-reactalongside the existingChevronRight,Loader2. The button is always rendered, including on collapsed cards, so onepress goes from collapsed to raw.
State.
const [raw, setRaw] = useState(false);insideToolCallCard—per card, ephemeral, no store surface. Import
useStatefromreact.Body. Replace the body block (currently lines ~113-119) with:
RawToolViewis passed the outerargsandstreamdestructured fromprops, nevereffectiveArgs/effectiveProps.stream. Forwrite→xd://<tool>calls,resolveXdevWrite(lines 148-191) synthesizes an innername, inner args parsed from
args.content, and a rebuiltresultwithpartialResultdropped; the raw view must show the call that actuallyhappened, not that reconstruction. For every other tool the two are identical.
Deliberately placed outside
ToolRenderBoundary, so raw still works when arenderer throws.
3.
RawToolViewcomponentNew file
apps/web/src/components/tools/RawToolView.tsx:Renders a
<div className="space-y-1.5">with two labelled sections, usingSectionLabelfrom./shared(<div className="meta mt-2 mb-1">) rather thanthe
<details>idiom — raw is already an explicit mode, so a second click toreveal would be redundant:
<SectionLabel>args</SectionLabel>then<CodeBlock code={formatRawJson(args)} copyText={() => JSON.stringify(args, null, 2)} language="json" />.argsis always an object at this boundary; an empty one renders as{}.Result section. Let
const result = stream?.result; const partial = stream?.partialResult;result !== undefined: labelresult, bodyformatRawJson(result).partial !== undefined: labelpartial, bodyformatRawJson(partial).result, and instead of aCodeBlockrender<div className="meta text-ink-4">{stream ? "no result yet" : "no stream data"}</div>.streamisundefinedfor a tool call block whose id never reached thetoolCallsmap;resultis absent while a call is still running.The result
CodeBlockgetscopyText={() => JSON.stringify(result ?? partial, null, 2)}.Both blocks use
language="json".CodeBlockcallshljs.highlight(text, { language, ignoreIllegals: true }), so theintentionally-invalid JSON cannot throw; it may highlight oddly past a line
break (unverified — check visually during implementation). If the highlighting
is visibly broken on multi-line payloads, drop
languagefrom these two blocksso
CodeBlockfalls through tohljs.highlightAuto; do not change theformatter output to accommodate the highlighter.
No
max-h-*override is passed — theCodeBlockdefaultmax-h-72applies.4.
copyTextpassthrough onCodeBlockIn
apps/web/src/lib/code.tsx, add to thePropsinterface (lines 41-47):Destructure it in
CodeBlockand passtext={copyText ?? text}to bothCopyButtoncall sites — the oversized/binary branch and the normal branch.CopyButtonalready acceptsstring | (() => string)(lib/CopyButton.tsx),so no change there. All existing
CodeBlockcallers omit the prop and keepcopying their displayed text.
This makes the clipboard carry complete, valid, unelided JSON — including full
base64 image data — while the screen shows the readable, elided render.
Verification
Run from the repo root.
cd apps/web && bunx tsc -b --noEmit— compiles clean.New test file
apps/web/src/lib/raw-json.test.ts, matching the existingbun:testconvention oflib/git-format.test.ts(import { describe, expect, test } from "bun:test").apps/webhas notestscript and no DOM harness, so run it withcd apps/web && bun test src/lib/raw-json.test.ts. Cases:JSON.stringify(v, null, 2)byte for byte.{"content":[{"type":"text","text":"total 12\ndrwxr-xr-x 3 lucas\n"}],"isError":false}example from step 1a produces the exact expected string, including the
15-space continuation indent and the lone trailing-quote line.
opening quote, not to a key's.
\t,\"and\\inside a rendered multi-line string remain escaped.{ type: "image", data: "A".repeat(500), mimeType: "image/png" }renders"data": "‹image data elided: 500 chars›"and leavesmimeTypeintact.type: "text"and a longdatastring is not elided.formatRawJson(undefined) === ""andformatRawJson(null) === "null".Manual, in a live session (
just devor the existing dev scripts; open theweb UI on a session with history):
bashtool call, press the braces button: body swaps toargs+result, and the shell output insideresultreads as realindented lines, not one
\n-laden line. Press again: the normal rendererreturns.
into the raw view.
forgejo_*orgit_*call, confirmresult.detailsis visible inraw — it is dropped by
extractResultTextin the rendered view, so thisis the concrete new capability.
write→xd://deck_task_listcall, confirm raw showspath: "xd://deck_task_list"and thecontentJSON string, i.e. the outercall, while the rendered view still shows the inner
deck_task_listcard.browserscreenshot orgenerate_imagecall, confirm the raw resultshows the elision placeholder and renders instantly.
is valid parseable JSON with
\nescapes restored and full base64 present.Assumptions & contingencies
so raw is equally complete for a reloaded session as for a live one
(unverified — the SDK owns that file format and was not inspected). If a
reloaded session shows a thinner
resultthan the live one, that is an SDKstorage limit, not a defect in this feature; leave it and report it.
transcript remounting proves to lose the toggle often enough to annoy, move
the flag into the
toolViewstore slice asrawPerCard: Record<string, boolean>with a
setToolCardRaw(id, raw)action mirroringsetToolCardOpen(
lib/store.ts:104-111), and read it the same wayopenis read.