Files
deepseek-harness/.agents/notes/implemented/feature/2026-07-26-code-mode-chat-subcall-rows.md
Tianyi Cui 13f7c62318 feat(web): render Code Mode sub-calls as native rows nested under the run_code row
The client indexes tool/code-dispatch events into
ConversationSnapshot.codeDispatches (parent callId -> ToolResultNode-shaped
sub-calls; live mux and history replay build the identical index). ChatView
renders each run_code parent as the new code variant (description summary,
program as the expanded monospace body) with its sub-dispatches as
always-visible indented rows — every sub-row dispatches through the SAME
keyed conversation.chat.toolview hole with the same GenericToolCard
fallback, so custom registrations (bash sample) take over sub-rows exactly
as top-level rows. The details panel resolves sub-callIds to full logged
args and complete output through the native path.

Evidence: fixture turn 64 + built-bundle jsdom snapshot, real-machinery
jsdom suites (nesting, error state, details, running parent, reference
stability), and a recorded code-mode browser e2e round (keyless replay +
aria golden). Scaffold gains a toolsMode patch knob.
2026-07-26 04:02:38 +08:00

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_code turn — the client-side half of the Code Mode UI stack, built on the host foundation (full-content tool/code-dispatch, the required description parameter). 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.applyEventSideEffects folds each in-window tool/code-dispatch into ConversationSnapshot.codeDispatches: ReadonlyMap<parentCallId, readonly CodeSubCall[]>, where CodeSubCall IS ToolResultNode (the sub-call id as callId, the logged args JSON-stringified into call.argsRaw, the full logged content/isError). Live mux frames and history replay build the identical index (rebuildDerivedFromWindow clears and re-derives; copy-on-write per-parent arrays keep snapshot references memo-stable). Sub-calls never join nodes — 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/client Context merges collide), the same posture as every cross-wire payload.
  • Render layer: ChatView's CallRow renders the parent, then — for parents present in the index — a [data-subcalls] nest of SubCallRows, each dispatching through the SAME 'conversation.chat.toolview' keyed hole with entryKey = sub-tool name and the same GenericToolCard fallback. Identity with native rows holds by construction: a keyed registration (e.g. the bash sample) takes over sub-rows exactly as it takes over top-level rows, with zero registration changes. Running parents (runningCalls) nest their so-far dispatches the same way, so sub-rows stream in live during the run (PR1 logs each dispatch as it completes).
  • run_code presentation: a new code row variant (classifier run_code → code, Code title, IconCodeOutline16) summarizes with the model-authored description and expands to the program itself (monospace on the markdown code-block fill) rather than the args JSON envelope.
  • Details panel: materialFor falls 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.