fix(chat): draw the single-chat row set in split view panes - #3302
Conversation
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Design Review (Fable 5) — 🟡 CONCERNSAdvisory design-level review of I have what I need. The PR is a single frontend commit: a new Design-Verdict: CONCERNS Right extension seam, but it forks the single-chat row set into a second hand-maintained copy that ChatPage itself never consumes — drift is now silent. Watch
Suggestions
[DESIGN-REVIEWED] cba9e97 |
UX Review (Fable 5) — 🟡 CONCERNSAdvisory UX-level review of Evidence gathered: I've read the diff, both screenshots, and traced the click handlers of the cards this PR newly draws in panes ( UX-Verdict: CONCERNS Panes now show the right rows, but the launch cards' click-through opens the anchor session's panel — the label's promise breaks in the exact surface this PR targets. Watch
Suggestions
[UX-REVIEWED] cba9e97 |
Opus 4.8 Review — ✅ no blocking findingsReviewed Review detailsThe evidence is conclusive. The backend persists tool calls as role The candidate's part (a) — a concrete input that occurs in practice — cannot be established. Its own path narration ("If such rows ever reach the pane's message array") is a "might," and establishing it requires assuming messages the backend never produces. The No findings. [OPUS-REVIEWED] cba9e97 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
The shared transcript's registry defaults are deliberately store-free, so a split view pane could not draw any row needing live app state: thinking traces, sent files and auto-nudge turns drew nothing at all, workflow and sub-agent launches collapsed to a generic pill, and recovery injects, workflow completions and stop events fell back to weaker rows. Supply those rows as host registry entries instead, which is the registry's documented extension path and keeps app-sdk/ChatMessageList Redux-free for the embed SDK. The two launch rows reuse the shared isWorkflowRunTool / isSpawnRunTool predicates the grouping logic uses, so a launch card and TurnBlock can never disagree about whether a row is a launch. Tool disclosure moves above the rows so it survives a remount. Fixes #3299
926c092 to
cba9e97
Compare
|
The same file passes locally on this commit, 82/82. Mochi's panel is the self-owned vendored fork with its own role dispatch — neither that test nor
Re-run is pending: the workflow still has jobs in flight, and |
bolichen97
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: small-fix (5 files). Criteria: no conflict, no requested changes, no security surface, AI reviewers green. Category: small-fix — corrects rendering of single-chat row set in split view panes.
|
Both advisory reviews on this PR are dispositioned in the follow-up: #3334 UX Review — FIXED. The finding was correct and it was a regression this PR introduced: Design Review — partly fixed, partly filed. The drift concern now has a mechanical guard instead of a promise: a test extracts every role the single-chat chain dispatches on out of Also fixed there: both launch cards lay out their own full-width row, so wrapping them in
|
The workflow and sub-agent launch cards deep-link into the Workflows / Subagents side panel. That panel is mounted for activeSlot, and split view deliberately never moves activeSlot with pane focus, so #3302 — which started drawing these cards in panes — gave a background pane a click that opens a DIFFERENT session's panel, usually reading "No subagents running". Split view exists to watch background sessions, so most clicks hit it. Make the click correct instead of removing it: opening from a pane whose session is not active dispatches switchSlot first, so the panel that opens is the one the card's label promises. Safe inside split view — the auto-enter effect is gated on splitMode being off, so switching neither reseeds nor leaves the grid. Single chat passes no slot and is unchanged. An earlier revision instead dropped the affordance in background panes. UX review was right that this trades a lying link for a dead end: a user seeing "1 agent failed" in the surface split view exists for would have no route to the detail and no cue that one exists, and the card keeps enough of its accent shell that a habituated click reads as broken. Retargeting removes the class rather than hiding it, and needs no new copy — "Open in the Subagents panel" stays true. Also drops a double row wrapper: both cards lay out their own full-width row, so wrapping them in the registry's ctx.row doubled the padding. Adds a drift guard pinning that every role the single-chat chain dispatches on is claimed by the registry a pane renders through, and names the tracked consumer of each option that has no pane wiring yet. Refs #3302, #3332
#3334) The workflow and sub-agent launch cards deep-link into the Workflows / Subagents side panel. That panel is mounted for activeSlot, and split view deliberately never moves activeSlot with pane focus, so #3302 — which started drawing these cards in panes — gave a background pane a click that opens a DIFFERENT session's panel, usually reading "No subagents running". Split view exists to watch background sessions, so most clicks hit it. Make the click correct instead of removing it: opening from a pane whose session is not active dispatches switchSlot first, so the panel that opens is the one the card's label promises. Safe inside split view — the auto-enter effect is gated on splitMode being off, so switching neither reseeds nor leaves the grid. Single chat passes no slot and is unchanged. An earlier revision instead dropped the affordance in background panes. UX review was right that this trades a lying link for a dead end: a user seeing "1 agent failed" in the surface split view exists for would have no route to the detail and no cue that one exists, and the card keeps enough of its accent shell that a habituated click reads as broken. Retargeting removes the class rather than hiding it, and needs no new copy — "Open in the Subagents panel" stays true. Also drops a double row wrapper: both cards lay out their own full-width row, so wrapping them in the registry's ctx.row doubled the padding. Adds a drift guard pinning that every role the single-chat chain dispatches on is claimed by the registry a pane renders through, and names the tracked consumer of each option that has no pane wiring yet. Refs #3302, #3332
…ev#3302) The shared transcript's registry defaults are deliberately store-free, so a split view pane could not draw any row needing live app state: thinking traces, sent files and auto-nudge turns drew nothing at all, workflow and sub-agent launches collapsed to a generic pill, and recovery injects, workflow completions and stop events fell back to weaker rows. Supply those rows as host registry entries instead, which is the registry's documented extension path and keeps app-sdk/ChatMessageList Redux-free for the embed SDK. The two launch rows reuse the shared isWorkflowRunTool / isSpawnRunTool predicates the grouping logic uses, so a launch card and TurnBlock can never disagree about whether a row is a launch. Tool disclosure moves above the rows so it survives a remount. Fixes kirodotdev#3299
kirodotdev#3334) The workflow and sub-agent launch cards deep-link into the Workflows / Subagents side panel. That panel is mounted for activeSlot, and split view deliberately never moves activeSlot with pane focus, so kirodotdev#3302 — which started drawing these cards in panes — gave a background pane a click that opens a DIFFERENT session's panel, usually reading "No subagents running". Split view exists to watch background sessions, so most clicks hit it. Make the click correct instead of removing it: opening from a pane whose session is not active dispatches switchSlot first, so the panel that opens is the one the card's label promises. Safe inside split view — the auto-enter effect is gated on splitMode being off, so switching neither reseeds nor leaves the grid. Single chat passes no slot and is unchanged. An earlier revision instead dropped the affordance in background panes. UX review was right that this trades a lying link for a dead end: a user seeing "1 agent failed" in the surface split view exists for would have no route to the detail and no cue that one exists, and the card keeps enough of its accent shell that a habituated click reads as broken. Retargeting removes the class rather than hiding it, and needs no new copy — "Open in the Subagents panel" stays true. Also drops a double row wrapper: both cards lay out their own full-width row, so wrapping them in the registry's ctx.row doubled the padding. Adds a drift guard pinning that every role the single-chat chain dispatches on is claimed by the registry a pane renders through, and names the tracked consumer of each option that has no pane wiring yet. Refs kirodotdev#3302, kirodotdev#3332
1. What is the problem?
A Split View pane renders its transcript through the shared list
website/src/app-sdk/ChatMessageList.tsx, which resolves every row through the registry inwebsite/src/app-sdk/messageRenderers.tsx. Those defaults are deliberately store-free — that module must stay importable by consumers running outside the dashboard's React root, which have no Redux store at all.So the defaults cannot draw any row that needs live app state, and a pane showed a reduced transcript against single chat. Four row types drew nothing whatsoever:
thinkingThinkingBlockwith the reasoning textundrawn)fileFileCard— player / downloadnull)nudgeNudgeCardwith the cycle chipWorkflowRunCard+ live run statusSubagentRunCard+ live agent badgesinjectRecoveryCard, one lineWorkflowCompletionCard, compactstop_eventStopEventCarddivA tool row's expanded state was also held inside the row, so it was forgotten on every remount.
2. Why this issue matters to the user
Split View exists to watch several sessions at once, and the dropped rows are disproportionately the ones carrying what a background session is doing right now. A pane is exactly where you want to see a workflow launch, a wave of sub-agents, an auto-nudge cycle firing, or a recovered turn — and those rendered as nothing or as an unlabelled pill, so a stalled session looked the same as a working one.
Two gaps read as product bugs rather than missing renderers: a sent file simply never appeared, and an auto-nudge turn's prompt was invisible, making a live monitoring loop look idle.
3. How our fix solves it
The symptom is missing rows; the root cause is where the rows may live. The registry is store-free by contract, so the fix is not to teach it about Redux but to supply the store-connected set from the host, which is the registry's documented extension path (
docs/app-kit/api-reference.md): "anything that genuinely needs live app state is supplied BY the host as a registry entry".website/src/pages/chat/transcriptRenderers.tsxcarries the single-chat row set as 11MessageRendererentries. It sits underpages/chat/precisely because it reaches for app state —app-sdk/ChatMessageList.tsxstays Redux-free for the embed SDK.ChatPaneswitches from the single-purposerenderToolshorthand to the fullrenderersprop. The SDK'srenderToolsupport is untouched and still exercised by its own test.ChatPaneholds the map, the entry reads it), so expansion survives the remount that a message update causes.isWorkflowRunTool/isSpawnRunToolpredicates that the grouping logic already uses, so a launch card andTurnBlockcan never disagree about whether a row is a launch.One consequence worth naming: this module replaces both of the registry's shape-matched defaults (
stop_event,subagent_completion). AftermergeRenderersthere are no shape-matched defaults left, so the "shape beats role" guarantee is carried by this module's own array order. That is now pinned by test rather than left to a comment.4. What tests we did
New
website/src/test/transcriptRenderers.test.tsx— 16 tests:null-returning entry would look identical on screen);inject, workflow completion over plainassistant);🔧guard survives, so the hidden🚫deny sibling is still never drawn — including when its output is launch-shaped;nudge,error,file,assistantornoticestill draws as a stop event;queued/systemstill resolve to an entry that draws nothing;Gates, all green:
tsc --noEmit0 errors ·eslint0 errors (6 pre-existing warnings inChatPane, untouched lines) ·npm run i18n:checkOK ·npm run i18n:renderOK · fullvitestrun: 1066 test files passed.No new user-facing strings, so no locale catalogue changes.
Evidence
Captured on an isolated pod built from this branch, in a real Split View pane.
The pane, whole — its own header with the split/close controls, its own composer and status strip, so this is unmistakably a pane and not the single-chat surface. The sub-agent completion card (
2 agents finished · Open Subagents panel) is one of the rows this change wires up:The same pane with the tool group expanded. These are store-connected
ToolCallLinerows — backend-stamped purpose text and per-row status glyphs (⊘for the two sandbox-denied reads,⊙for the completedspawn_runload) — where the default registry would draw a static pill:Not demonstrated in these captures, and why: the thinking, file, auto-nudge, workflow-launch, workflow-completion, recovery and stop-event rows. The model in this pod emitted no separate thinking row (the "Thinking through the steps" text is ordinary assistant content), and the others need a real workflow run, a live monitor loop or a
file_sendto exist. Those rows are covered by the unit tests above rather than by a screenshot.Visible in the captures and NOT introduced here: the empty
🔧 0 tool callsboxes. Those are groups containing onlypermissionmessages —CollapsibleToolGroupis givencount={nonPerm.length}, which excludes permissions and so reads 0. Single chat never shows them at all, becausegroupDisplayItems.tsdropspermissionrows outright. This predates the change (a pane already grouped the same way) and is not fixable from the registry: the group is assembled before per-row resolution, which is the #2940 limitation. It belongs to the grouping convergence noted below.5. Any other suggestions on the work
Deliberately out of scope, with reasons:
thinkingstill renders inside the collapsible group. It is inGROUPED_ROLES, so a host entry claiming it resolves within the group instead of standalone as single chat draws it. Opting a grouped role out of the group is not an extension point yet — chat SDK: let a host renderer opt a grouped role out of the collapsible group #2940. The entry is still a strict improvement (content shown vs nothing), and the limitation is documented at the entry.usePanelTabs(activeSlot)-keyed while pane focus deliberately never routes throughactiveSlot, and two pieces ofChatPage's handler (search.close(), inline-preview de-duplication) are component-local and cannot be reproduced from a pane, so a copy would silently fail to show the file whenever the find pane is open.selectContinuable/selectTurnInterrupted; both areactiveSlot-only today. The plumbing is in place behind optional options.AssistantMessagetakes 31 props in single chat against the SDK default's 10.Observed while working here, worth a separate look: the transcript has two independent grouping implementations —
ChatMessageList's inline pass andpages/chat/groupDisplayItems.ts— and they disagree on whetherthinkingis groupable and on whethernudgeopens a turn. Open PR #3207 is currently applying the same fix to both files, and the copy landing in the SDK omits thenudgeboundary the other one has. Not touched here to keep this diff reviewable.Collision note:
ChatPane.tsxis also touched by open #3265, #3240 and #2243, so this change was kept deliberately small there (+26/−9, confined to the imports, one new memo, and the call site) to stay rebase-friendly.Fixes #3299