4.5 KiB
Agent Note: Code Mode chat rendering — sub-calls as native rows under the parent
Status: implemented
English | 中文
Scope: how the web chat view renders a
run_codeturn — the client-side half of the Code Mode UI stack, built on the host foundation (full-contenttool/code-dispatch, the requireddescriptionparameter). The toolview dissolution owns the slot model this rides on.
Problem
With Code Mode enabled, the chat view showed one opaque run_code row: raw program text as the summary, sub-calls invisible everywhere. The settled product requirement is the opposite: each sub-call must render identically to a native tool call — same row components, same custom registrations, same details panel — while the transcript stays honest about the fact that the model made ONE call.
Decision
Sub-calls are ToolResultNodes indexed off the surface flow, rendered through the same keyed slot as native rows, nested always-visible under their parent.
- Data layer:
Session.applyEventSideEffectsfolds each in-windowtool/code-dispatchintoConversationSnapshot.codeDispatches: ReadonlyMap<parentCallId, readonly CodeSubCall[]>, whereCodeSubCallISToolResultNode(the sub-call id ascallId, the logged args JSON-stringified intocall.argsRaw, the full loggedcontent/isError). Live mux frames and history replay build the identical index (rebuildDerivedFromWindowclears and re-derives; copy-on-write per-parent arrays keep snapshot references memo-stable). Sub-calls never joinnodes— the surface flow remains exactly the model-visible turn structure. The event is narrowed structurally at the wire-consumer boundary (dsh-tools' host types cannot enter the client program — the host/clientContextmerges collide), the same posture as every cross-wire payload. - Render layer:
ChatViewpasses each parent and its indexed children through the whole-Tool'conversation.chat.tool'seat. ui-tool'sToolCallTreerenders the parent followed by a[data-subcalls]nest, and every atomic call dispatches through the same'tool.call.toolview'keyed slot withentryKey = Tool nameand the sameGenericToolCardfallback. A keyed registration therefore takes over child and top-level calls without registration changes. Running parents (runningCalls) receive their accumulated dispatches through the same owner payload, so child rows stream in during the run. run_codepresentation: a newcoderow variant (classifierrun_code → code,Codetitle,IconCodeOutline16) summarizes with the model-authoreddescriptionand expands to the program itself (monospace on the markdown code-block fill) rather than the args JSON envelope.- Details panel:
materialForfalls through nodes → runningCalls → the dispatch index, so a selected sub-callId resolves to full args and complete output through the identical rendering path as a native settled call.
Alternatives considered
Sub-calls flat in the surface flow (fold them into nodes). Rejected: misrepresents the transcript — the model made one call; nesting under the parent preserves the code↔calls association and keeps the fold's model-visible-order invariant untouched.
Hidden until the parent row expands. Rejected by product decision: the sub-calls ARE the story of a Code Mode turn; hiding them re-creates the opacity this feature removes. The parent's expand toggle reveals only the program.
A dedicated sub-call row component. Rejected: the whole point is identity with native rows; a parallel component would drift. The nest wrapper (indent + left edge) is the only sub-call-specific chrome.
Consequences
Custom toolview registrations apply to sub-calls for free — and deliberately: there is no per-registration opt-out short of the component reading its own context, which no current consumer needs. Selection highlighting reaches nested rows through the same selectedCallId channel (group membership tests both levels). Trajectory/waterfall still render run_code as a single row — their sub-call spans are deferred to the PR that adds dispatch timing (start/end events), without which a waterfall span would be a lie. Fixture turn 64 (?fixture) plus the code-mode-round browser e2e (recorded real round, keyless replay) pin the full surface; the jsdom suites pin the slot dispatch, error states, details resolution, and index reference stability.