refactor(chat-core): one dashboard row set -- ChatPage spreads createTranscriptRenderers - #8733
Conversation
UX Review (Fable 5) — ✅ PASSUX-level review of UX-Verdict: PASS Pure renderer consolidation: every row a user sees renders identically, and parity is pinned by contract tests rather than left to intent. I traced each apparent narrowing against its predicate: [UX-REVIEWED] b5be62e |
Design Review (Fable 5) — ✅ PASSDesign-level review of Design-Verdict: PASS The two hand-kept row lists were a proven defect source; converging them into one factory with pane-default options is the right shape, and the reversal of #3332 is recorded, not silent. The equivalences the "no output change" claim rests on check out against the code, not just the description: [DESIGN-REVIEWED] b5be62e |
First Principles Review (Fable 5) — ✅ PASSPremise-level review of Verification complete. Both factory consumers counted (ChatPane at First-Principles-Verdict: PASS Deletes the second hand-kept row list — the named cause of the What this change shipsIntent: make the dashboard's rich chat rows exist once so a row fixed once reaches the page and every pane — a FIX for a recorded defect class (RFC P5-b, #8713 review).
The direction is the one this lane exists to reward: net deletion (~120 page lines out, ~60 factory lines in), a second spelling removed rather than guarded, and the one recorded decision it overturns (#3332) is named in the description with the defect that overturns it. Remaining split from the store-free [FIRST-PRINCIPLES-REVIEWED] b5be62e |
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: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
062d7a0 to
fabb37a
Compare
|
Design — Suggestion: fold Applied. First Principles — Watch: the no-screenshot waiver rests on the unpinned claim that Pinned. The parity contract now enumerates the SDK defaults whose |
fabb37a to
85ef5fd
Compare
|
First Principles — Subtraction: drop the Applied. Design — Suggestion: fold the page-only Applied. |
|
First Principles — Subtraction (deferred, as you frame it): Agreed and recorded as a follow-up: once this lands, a small PR makes an unparseable |
8e10fda to
7e823e0
Compare
85ef5fd to
e66bf1b
Compare
e66bf1b to
6b87cb9
Compare
6b87cb9 to
43938f6
Compare
…TranscriptRenderers After P5-a the dashboard's rich transcript rows lived in two hand-kept host lists: ChatPage's and pages/chat/transcriptRenderers.tsx's createTranscriptRenderers (ChatPane's). They shared ids but not code, so a row could still diverge between the page and a pane. The factory now carries the page's behaviours behind options with the pane's defaults -- toolDisclosureKey (the #8204 tool_call_id fold), toolRunning (the page's trailing-group rule), transcriptHot, and the completion cards' session hand-offs (onSessionOpen / sessions / activeSession); `slot` becomes optional (the page reads the active slot). ChatPage's host list is the factory spread plus its page-only rows: the bubble, stop_event, notice, permission, the narrow undrawn set, mcp_oauth, the hidden invisible-assistant skip, the file variant that falls through to the bubble (ahead of the spread, so no row's output changes), and tool_completion (the deny/complete sibling draws nothing; claimed because this page's unclaimed-role fallback is the bubble). ctx.row / ctx.wrapper are keyed Fragments, so a shared row lands in the DOM exactly as the page's own entry did. Tests: transcriptRenderers.test's role-by-role drift guard against the page's if-chain becomes a structural check that the page spreads this factory and keeps no private copy of a shared row; the parity contract counts the factory's ids as host entries; RecoveryCard's shape test follows the entry into the factory. All ChatPage and ChatPane suites pass unchanged. RFC chat-core extraction, P5-b (stacked on P5-a, #8713).
43938f6 to
b5be62e
Compare
chenmingwei23
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: refactor (6 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: refactor -- code-move only, ChatPage spreads createTranscriptRenderers ahead of page-only rows, prop-set-equivalent entries, no rendered output change on either surface.
iamwhatever
left a comment
There was a problem hiding this comment.
Tier 1 auto-approve: refactor (6 files). Criteria: no conflict, no requested changes, security path denylist clean, design-doc gate clean, SAST annotations clean, security checklist all-NO, AI reviewers green. Category: code-move only -- ChatPage stops carrying its own duplicate renderer if-chain and spreads the shared createTranscriptRenderers row set extracted into website/src/pages/chat/transcriptRenderers.tsx, with the four accompanying test files re-pointed at the new module; no runtime behaviour change, no backend surface, no auth/session path touched.
Problem / Motivation
After P5-a (#8713) ChatPage dispatches rows through the app-sdk registry — but the dashboard's rich rows (tool line + launch cards, thinking block, nudge, recovery inject, the two completion cards, the error card with Continue) still lived in two hand-kept host lists: ChatPage's own, and
pages/chat/transcriptRenderers.tsx'screateTranscriptRenderers, which ChatPane consumes. #8713's review named this precisely: the mechanism was unified, the row set was not, and themcp_oauthdefect class survived between the two lists. The factory was also the weaker variant — tool disclosure keyed by row key rather thantool_call_id(#8204), "running" animation off the transcript flag rather than the page's trailing-group rule, notranscriptHot, no session hand-offs on the completion cards — so a pane drew a reduced transcript by construction.Why it matters
RFC chat-core extraction P5-b. One dashboard row set: a row added or fixed once reaches the page and every pane, and a pane stops being the surface that silently gets less. Stacked on #8713 because it rewrites the host list that PR introduced.
What changed (motivation → approach → change)
The factory learns the page's behaviours (
TranscriptRendererOptions). Two are unconditional because they are pure and no surface wanted the old default: the tool row's disclosure identity is now the #8204tool_call_idfold for every surface (toolDisclosureKeymoves out ofChatPage.tsxinto the factory and is exported from there — a pane's row keys already embed the index, so its output does not change), and an unparseablefilerow draws whateverrenderUnparsedFile(m, ctx)returns (default nothing, the pane's behaviour; ChatPage passes its bubble, the if-chain's fall-through). The rest are options with the pane's defaults:toolRunning(m, ctx)(default:ctx.running; ChatPage passes the trailing-group ruleslotState === 'tool_running' && index > lastTextIdx),transcriptHot, andonSessionOpen/sessions/activeSessionfor the two completion cards.slotbecomes optional — omitted, the tool line and launch cards read the active slot, which is what the page did. ChatPane's call is unchanged and every default reproduces what it drew before.ChatPage's host list is the factory spread plus page-only rows, in this order:
...createTranscriptRenderers({...})— which now includestool_completion(the ✅/🚫 sibling draws nothing on every surface; a pane's fallback already drew nothing, the page's is the bubble, so claiming it in the factory closes "✅ done as a bubble" for both) —stop_event,notice,permission, the narrowundrawn,mcp_oauth,hidden_invisible_assistant, and thebubble. Nine page entries are deleted (fileincluded — its one difference is therenderUnparsedFileoption) (thinking_block,tool,nudge,recovery_inject,error,workflow_completion,subagent_completion), along with nine now-unused imports and the page'slastErrorIdxmemo (the factory derives the newest-error index from the transcript it is handed).ctx.row/ctx.wrapperare keyedFragments, not elements: the factory returnsctx.row(<ToolCallLine …/>), and the row has to land in the DOM exactly as the page's own entry rendered it (the virtualizer measures the row's component root).ctx.wrapperis reached by no row on this page — the SDK defaults that use it (user,assistant,inject) claim roles the page'sbubbleentry claims first — and the parity test now pins that (it enumerates the wrapper-using defaults from the registry and checks the bubble's role list covers them), so the "no pixel to show" claim is asserted, not assumed.No row's output changes on the page: every option ChatPage passes reproduces the prop set its deleted entry passed (checked entry by entry — tool line, launch cards, thinking block, nudge Loop gating, recovery card, completion cards' hand-offs, error Continue gating on
continuable && interrupted && index === lastErrorIndex). ChatPane's output is unchanged because every new option defaults to the old behaviour.A recorded decision this supersedes.
transcriptRenderers.test.tsx's old drift guard carried a comment that converging the two row sets by moving ChatPage onto the registry "was considered and rejected (#3332, closed not-planned): the single-chat surface has no problem to fix". The RFC's §2.2 names the problem (mcp_oauthshipped registered-but-raw in the main chat because there were two lists), P1 introduced the registry and its parity test for exactly that, and P5 is the phase that removes the second list; the guard's comment is rewritten to say so. Flagging it here so the reversal is explicit, not silent.Tests
transcriptRenderers.test.tsx: the role-by-role "drift guard against the single-chat row chain" (which parsed ChatPage's.role ===literals) becomes a structural guard — ChatPage imports and spreadscreateTranscriptRenderersahead of its page-only rows, and keeps no private copy of a row the factory draws (zero exceptions).toolDisclosureKey.collision.test.tsimports the helper from its new home;toolDisclosureKey.renderSite.test.tsx(the Same-tick tool rows share one disclosure key (messageRowKey) #8204 render-site pin) passes unchanged.chatRolesParity.contract.test.ts: host-entry ids now include the factory's when the page spreads it; the documented non-default ids gain the factory's (workflow_run_tool,subagent_run_tool,tool_completion); a new assertion pins that every SDK default rendering throughctx.wrapperis shadowed by the bubble.transcriptRenderers.test.tsx: the deny-sibling guard now asserts the sibling resolves totool_completionand rendersnull(it used to assert "unclaimed").RecoveryCard.test.tsx: the card wiring is asserted in the factory and the spread asserted in the page.ChatPage*suites and allChatPane*suites,transcriptRenderersRenderCov80,TranscriptRowGeometry,TurnBlock.rowMemo,ChatPageCoverage(the✅ donetool-completion row still draws nothing) — 890 tests across 89 files.Local gates:
tsc -b, eslint on changed files,check-i18n-strings(0 added). The full vitest suite runs in CI.Manual verification
N/A — a dispatch/ownership refactor with prop-set-equivalent entries and deterministic coverage; no rendered output changes on either surface.
Why no screenshot: every option ChatPage passes reproduces the prop set of the entry it replaces, and every new factory option defaults to ChatPane's previous behaviour; there is no pixel to show on either surface.
Related Issues
ChatInputseam).Checklist
Contribution License Agreement
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.