Skip to content

refactor(chat): split chat page controllers - #7255

Open
bolichen97 wants to merge 1 commit into
mainfrom
refactor/chat-page-boundaries
Open

refactor(chat): split chat page controllers#7255
bolichen97 wants to merge 1 commit into
mainfrom
refactor/chat-page-boundaries

Conversation

@bolichen97

@bolichen97 bolichen97 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Problem / Motivation

ChatPage.tsx had grown into an approximately 8k-line page where URL/session synchronization, composing, sending, transcript rendering, resources, and view chrome were interleaved.

Why it matters

The page now has explicit ownership boundaries without changing its route, public props, Redux/WebSocket contracts, streaming order, IME handling, scroll anchors, or responsive composition.

What changed

  • Reduced ChatPage.tsx to a readable composition layer and extracted cohesive session, composer, action, resource, transcript, message-content, and view modules.
  • Added and updated characterization/source-contract tests for session identity, drafts, mobile panels, pinned prompts, title sizing, streaming/voice, transcript pinning, and view-composition seams.
  • Registered two moved ChatPage notification strings in the existing i18n catalogs (all shipped locales plus regenerated pseudolocale) while pinning their legacy call sites to the original English bytes; machine paragraph delimiters remain byte-equivalent.
  • Preserved the client/server sendId wire expression verbatim: s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}; direct characterization locks its legacy output.
  • Repointed stale RFC citations from old ChatPage line numbers to the extracted owners' stable symbols, and documented the composer-to-actions forward-ref ordering that preserves voice/dropdown timing.
  • Removed the moved helpers' page-level re-export rather than keeping a compatibility facade: the website is a bundled SPA, so no out-of-repo importer can exist, and the single repository test that used the old path now reads the owning message-content module like its neighbours.
  • Added a direct foreground playwright-cli event characterization to keep the extracted page above the enforced per-file coverage floor without changing production behavior.
  • Kept ChatSidebar, ChatInput, chatSlice, useWebSocket, backend, Electron production code, dependencies, lockfiles, and CI configuration out of scope.
  • Rebased onto current origin/main and placed the three behaviours that landed on ChatPage.tsx while this was open by ownership rather than by where they were written. The composer footer's working-tree badge stays beside the project-git query whose repo flag gates it and whose cache key it shares, so the page keeps that derivation and hands ChatPageView three already-reduced counts; the per-session MCP report moves with ChatHeaderMenu into ChatPageMessageContent. The mobile drawer's history entry splits across two owners because it is two concerns — the mint/spend pair, the phase machine and the POP-closes-the-drawer effect stay with the drawer in the page, while the flag distinguishing a bookkeeping pop from a Back the user asked for is owned by useChatPageSessionController alongside the other pop refs, since that controller's ?sid effect is what reads it. Effect registration order is preserved end to end: the sid and URL-sync effects still run before the drawer's POP effect, so consuming the entry cannot resurrect the outgoing session. ChatPage.drawerBackClose.test.tsx (5/5), ChatInput.gitBadge.test.tsx and mcpSessionReport.test.tsx pass unchanged.
  • Named the extracted owners in the feature map's Sessions and Worktrees rows (docs/feature-map/README.md). The Feature Map Gate landed on main after this branch was cut and fires on a file appearing under website/src/pages/; ChatPage.tsx alone is no longer the answer to "where does this live". The Worktrees row is the same stale-pointer class as Sessions — api.createWorktree now lives only in useChatPageActionsController.ts (verified: that is the sole hit across ChatPage.tsx and pages/chat/*) — so fixing one row and not the other would have left the map half-true.
  • Took the union of both sides' keys on the three-way catalog conflict. No key either side added is dropped; this branch still adds exactly knowledge_context_was_not_kept and server_did_not_respond.
  • Reverted the one widened test bound, so ChatPageDrafts.test.tsx carries no timing change at all. An earlier round had raised the rapid-handoff waitFor from testing-library's 1s default to 5s, attributing the latency to a saturated coverage worker pool. That attribution was wrong, and it was worth measuring rather than asserting: instrumenting the FIFO handoff shows 244-267 ms on this branch versus 280-383 ms for main's monolith (3 runs each, same host, same file). The split added no render/commit boundary — the extracted page is if anything marginally faster — so the default bound has ~4x headroom and the hunk is gone. That removes the only place a reviewer could reasonably have read this diff as loosening a test.
  • Restated the comments that narrated the extraction instead of stating present behaviour, per the AGENTS.md Comments rule (no "previously / used to / we now" narration, no task-log markers). Five sites: the composer controller's rejected-send fallback, the actions controller's switchStability port docblock and its exhaustive-deps disable, the ChatPageView ownership note on the page, and the resources controller's docblock. Each now states the invariant rather than its history. The switchStability port itself is gone as of the latest rebasemain's refactor(frontend): burn the eslint warning ceiling to zero #7569 narrowed switchAgent's dependency list to what its body actually reads, which made that port and its exhaustive-deps disable dead code, so both are deleted rather than re-documented. I checked the whole diff for this class rather than the named instances, and deliberately left the narrating comments that are byte-preserved moves of main's own text (reflowed wrap points only) — rewriting those would enlarge the diff and break the byte-preservation contract that is this PR's point.

Tests

  • npm exec -- vitest run --coverage --pool=forks --maxWorkers=1 --no-file-parallelism ChatPage — 55 files, 489 tests passed.
  • Focused direct characterization: ChatPageView (3/3), transcript early controller (3/3), drafts (29/29 serially), legacy sendId (1/1), and message-content compatibility imports all passed.
  • 30 integration files / 306 tests passed.
  • npm run typecheck, final ESLint budget gate, jscpd, i18n catalog checks (19 checks), and docs lint passed.
  • Re-gated after rebasing onto the current origin/main tip, all exiting 0: npx tsc -b, npx eslint src/ --max-warnings 0 (0 warnings, 0 errors — main burned the ceiling to zero in refactor(frontend): burn the eslint warning ceiling to zero #7569; see the rebase note below), npx jscpd . (0 clones — the split introduced no duplication), npm run i18n:check (with I18N_BASE_REF at the merge base), npm run lint:phantom-classes (self-test + gate), check_feature_map.py, check_brand_name.py, and 86 vitest files / 872 tests passed covering every changed spec plus every ChatPage* spec. Two gates I did not re-run and am not claiming: the project-wide FRONTEND_MIN: 90 average (needs the full ~980-file suite) and check-bundle-size.mjs (needs an analyze build; an earlier round ran it on this tree at 739 chunks in budget). Both are low risk here — every touched module measures 83.0-97.6% line coverage against the 80% per-file floor, no baselined file graduates, the diff adds zero production dynamic imports, and vite.config.ts is untouched.
  • Re-gated after the fourth rebase (onto 1799bf192), everything below exiting 0 on this tree: npm run typecheck (tsc -b), npm run lint (eslint, 0 errors), npx jscpd . (0 clones), npm run i18n:check with I18N_BASE_REF=origin/main (19 checks · PASS), npm run lint:phantom-classes, check_feature_map.py, check_brand_name.py, check_harness_parity.py, check_black_formatting.py, check_subprocess_encoding.py, check_changelog_history.py, check_focus_cue.py, check_testpaths_coverage.py, check_builtin_skill_scope.py, check_loop_bound_locks.py, flake8 src/kiro_crew test, mypy --platform linux src/kiro_crew (1288 files, no issues), scripts/docs-lint.sh, and scripts/scrub-lint.sh --no-history under LC_ALL=C (see the inherited-failure note below — under a UTF-8 collation locale the [A-Za-z] range in its identity scan admits é, which is what makes main's /home/tést fixture look like a hit).
  • The full website vitest suite, not a subset: 1850 files, 29022 passed, 1 expected fail, 2 skipped, exit 0. This is the first round that claims the whole suite rather than the ChatPage* slice, because the two source contracts fixed in this rebase are ones a ChatPage-scoped selection would have missed.
  • The full offline Playwright suite against a harness gateway with the packaged fake ACP backend, plus a targeted causal check on fork.spec.ts (passes 3/3 at --retries=0; reproduces the CI failure when the ported deferral is unscoped).
  • check-bundle-size.mjs on a real analyze build — 806 chunks within budget, App chunk 3255.3 KB against the 3360 KB ceiling. This retires the "did not re-run" caveat above.

Manual verification

Automated characterization and source-contract coverage exercise the extracted seams. ChatPage Playwright cases require an authenticated agent gateway and can create sessions/send messages; this host has no safe isolated credentialed gateway, so they were not run.

Screenshots / video

No screenshot is included. This controller/view extraction preserves the existing layout and interaction surface; the extracted legacy notification sites are characterized to preserve their original English bytes across non-English and pseudolocale settings. No screenshot was fabricated against the user-owned gateway.

Why no screenshot: This behavior-preserving controller/view extraction leaves rendered layout, interactions, and legacy notification text unchanged. Screenshot-oriented ChatPage Playwright cases require a user-owned authenticated gateway, so no visual evidence was fabricated.

Rebase onto main (third time main moved under this PR)

main advanced 137 commits, 7 of which touched ChatPage.tsx — a semantic conflict, not a three-way
merge, because main keeps adding to a file this PR has emptied into controller modules. Every hunk main
added was re-placed by RESPONSIBILITY, carrying its WHY comments with it. Nothing was dropped: each landing
site is pinned below and verified present.

main change Responsibility Landed in
settingsPath() for the model picker's "set as default" (#8098) presentation / routing string built in JSX chat/ChatPageView.tsx
TranscriptScrollShell extraction (#7977) presentation — scroller skeleton + slot bodies chat/ChatPageView.tsx
steer receipt resolves the optimistic bubble (#7658) the mutation is composer/send wiring… chat/useChatPageComposerController.tsx
…and its .mutate({ slot }) call site …the call is a write-side action chat/useChatPageActionsController.ts
autoSendTick bump so an already-mounted page still auto-sends composer state (autoSendTick lives there) chat/useChatPageComposerController.tsx
switchAgent dependency narrowing (#7569) write-side action chat/useChatPageActionsController.ts
fork / plan-from-here resolve by message id (#8047) transcript row rendering chat/useChatPageTranscriptController.tsx
Fork / Plan hidden in embedded panes (#7752) transcript row rendering (adds embedded/popout args) chat/useChatPageTranscriptController.tsx
handleFork / handlePlanFromHere take messageId still page-level handlers pages/ChatPage.tsx
revealAppInPanel closes the find pane unconditionally page-level IPC/panel wiring pages/ChatPage.tsx
Preview-expand listener depends on closeSidebar page-level window/lifecycle pages/ChatPage.tsx

Two of main's hunks needed no carrying — both sides had already converged. main's
exhaustive-deps disables on handleFileOpen / handleFolderOpen are unnecessary here because the
resources controller hoists const closeSearch = search.close once, so the dep is a bare identifier
rather than a member expression; and main's drop of scrollToDisplayIndex from the nav-scroll dep
array is already how the transcript controller writes it. The same hoist is now applied in
ChatPage.tsx for revealAppInPanel and the source-reveal handler, which is why neither needs
main's disable comment either.

One comment was rewritten rather than copied. main's WHY on the preview-expand deps asserts
closeSidebar closes over only drawerX and drawerTravel; on current main it is
[runDrawerClose, consumeDrawerEntry], so that text is already stale there. The carried comment states
the real chain (both links are over a useMotionValue, a useCallback([]), navigate and a ref, so
nothing in it churns) instead of importing a false claim.

Render/effect ordering was reasoned about explicitly, since tsc cannot see it.
TranscriptScrollShell declares no hooks at all, so interposing it adds nothing to the effect
tree and cannot reorder mount effects; its children are still constructed by the parent, so
virt.measureRef / sentinel refs attach in the same commit and the same document order. DOM order is
byte-identical (header spacer → aboveRows → top sentinel → spinner → top spacer → rows → bottom
spacer → bottom sentinel → belowRows), scrollerRef and onScroll stay on the same element, and
scrollerStyle merges first so the shell's scroll contract still wins while paddingBottom survives.
The autoSendTick bump crosses a module boundary (set in the composer controller, consumed by the
actions controller's send effect) but is only ever read as an effect dependency, never synchronously in
the same render, so it cannot race. closeSearch is declared immediately after the resources
destructure, above every consumer.

Also in this rebase

  • main's eslint ceiling is now 0 (refactor(frontend): burn the eslint warning ceiling to zero #7569), which surfaced 6 pre-existing
    react-hooks/exhaustive-deps warnings the split had created: refs and setters that were local
    useRef/useState before extraction, and which eslint can no longer prove stable once they arrive as
    controller properties. All 9 missing deps are completed, not suppressed — each is a raw useRef
    result or a raw useState setter, so listing them is free and adds no churn. npx eslint src/ --max-warnings 0 exits 0.
  • main's scroll-shell characterization net follows the code. ChatPage.scrollShell.recipe.test.tsx
    and .render.test.tsx read source TEXT out of pages/ChatPage.tsx; the invocation they pin now lives
    in ChatPageView.tsx. The recipe suite's own header says it "follows the CODE, not the file", so
    ChatPageView.tsx (and the transcript controller, which owns the second loadingOlder consumer) joined
    its file list, and the render suite's one readFileSync was repointed. Every assertion is unchanged
    — including the exactly-one-invocation pin, the fail-loud read, and the loadingOlder count pin.
    scripts/mutation-check-scroll-shell.mjs was repointed the same way; it now reports the byte-identical
    verdict origin/main does (67/83 caught, the same 16 survivors, all of them type/JSDoc lines in
    main's own TranscriptScrollShell.tsx), where before the retarget it aborted at "anchor not found".
  • The i18n catalog conflict took the union of both sides' keys, verified per catalog rather than
    asserted:
    across all 14 files, main's 18-42 added keys and this branch's 2 are all present, with 0
    dropped from either side and 0 extra.
  • CodeQL Dev Fleet restart/sync give no progress feedback, so users fire them twice #639 is unchanged and still main's. Re-verified against the three-dot diff on this head:
    website/src/hooks/useBottomTerminal.ts — which holds the real tainted source,
    const mintId = () => Math.random().toString(36).slice(2, 14) at line 58 — is touched zero times by
    this branch. The only Math.random the diff adds is the sendId expression, a verbatim move locked by a
    characterization test. The existing rebuttal disposition stands; no suppression was added.
  • ./scripts/scrub-lint.sh --no-history fails on test/test_atomic_write_named_duplicates.py:155,157
    (/home/tést).
    Inherited: that file has zero hits in this branch's three-dot diff and both lines exist
    byte-identically on origin/main.

Rebase onto main (fourth time main moved under this PR)

main advanced 45 commits past the base this branch last carried, 5 of which touched
ChatPage.tsx — again a semantic conflict rather than a three-way merge, because main
keeps adding to the file this PR has emptied into controller modules. ChatPage.tsx resolved to
this branch's thin host and each of main's five hunks was re-placed by RESPONSIBILITY, carrying
its WHY comment with it. Nothing was dropped: every landing site is named below and verified
present in the tree.

main change Responsibility Landed in
slot-scoped transcript deferral (useSlotDeferredValue, #8581) transcript rendering — it owns the display-items mirror chat/useChatPageTranscriptController.tsx
session-switch history bookkeeping — pushed-entry key set, pending-key claim, the shared stale-sid repair both POP paths call (#8516) session / URL synchronization chat/useChatPageSessionController.ts
queued-send stash, and a cancel restore that merges recovered paths into the staged chips (#5591) write-side send and its recovery chat/useChatPageActionsController.ts
user-content render options object + the session triple, so a user row's /chat?sid=… link switches in place (#8510) user-message rendering… chat/ChatPageMessageContent.tsx
…and the caller that supplies the triple …the row renderer that owns the slot-title map chat/useChatPageTranscriptController.tsx
settingsPath({ tab: 'voice' }) for the voice-setup modal (#8261) presentation / routing string built in JSX chat/ChatPageView.tsx

One placement is not a straight copy, and the reason is a language rule rather than a taste
call.
renderUserContentCb now reads sessionTitles, so it had to move BELOW that memo: a
useCallback dependency array is evaluated in the render body, and declaring the callback above
the memo would read the binding before its initializer runs. Both consumers (the row renderer and
the controller's return) are further down, so this is a pure relocation, and the position carries a
comment saying why it is load-bearing rather than incidental.

Two source contracts had to follow the code — the same class the third rebase already handled
for the scroll-shell net. Both landed on main after this branch was last rebased, so CI has never
run either of them against the split:

  • pagination.earlierAdmission.test.ts reads all four automatic older-history triggers out of one
    source file. All four, and the admission ref they share, now live in
    useChatPageTranscriptController.tsx, so that is the file it reads. Every assertion is
    unchanged
    , including the two ordering pins and the exact gated === 2 count. One end anchor is
    now the deps-list PREFIX (}, [dispatch, earlierBarInView) instead of the whole array, so a dep
    added for an unrelated reason cannot read as a gate violation. 12/12 pass.
  • injectBubbleWhitespace.test.ts finds the transcript's single warn-tinted bubble by class and
    asserts it carries no whitespace-pre-wrap and does pass softBreaks. That bubble is now in the
    transcript controller's row renderer. 3/3 pass — including the "exactly one container" positive
    control, which is what keeps the other two from passing vacuously once markup moves.

Every previously-red lane on 847ecdcf, attributed

Ten checks were red. Nine are answered by the rebase or by this branch's own fix; the tenth is
dispositioned below.

Lane Verdict Evidence
Backend Tests (3.12, 4) main's, already fixed upstream Job 101125494842: all 10 FAILED lines name test/test_slot_close_recreation_race.py — 9 × Failed: Timeout >120.0s plus test_cleanup_ordinary_archive_still_saves_and_removes - AttributeError: '_Req' object has no attribute 'can_read_body' raised at src/kiro_crew/dashboard/handlers/_shared.py:142. That file arrived on main in c477f0b9d with a request double that never modelled can_read_body; 5bc2fc796 (#8536) and 35426b9ea (#8583) fixed and de-duplicated it, and both are ancestors of this head (exactly one can_read_body property remains, at test/test_slot_close_recreation_race.py:127). This diff contains no Python at all.
Backend Tests (Windows) (4) same cause, same fix Job 101125212737: 4 × worker 'gwN' crashed while running 'test/test_slot_close_recreation_race.py::…'. Windows overrides --max-worker-restart=0 and has no SIGALRM, so the same hang kills the worker instead of timing out.
Frontend Tests (4) this PR's, fixed here Job 101125494986: pagination.earlierAdmission.test.ts > the top sentinel checks it first, expected '' to match /if \(!earlierBarInView\(\)\) return/ — the contract was still reading pages/ChatPage.tsx. Migrated above. The whole website suite is now green on this tree: 1850 files, 29022 passed, 1 expected fail, 2 skipped, exit 0.
Frontend Coverage Merge downstream of that shard ci.yml declares needs: [frontend-test, changes] and merges the shard blobs, so a red shard exits it non-zero by construction.
Coverage Gate downstream aggregate It fails closed on its upstreams; nothing in it is independently red.
Bundle Size Gate main's drifted ceiling Job 101125494908: FAIL assets/App-DE1AGs-8.js: 3.15 MB exceeds its 3.13 MB budget by 20.9 KB (chunk 'App'). 449425a50 (#8519) re-measured that ceiling to App: 3360 * KB; on this head an analyze build measures the App chunk at 3255.3 KB and node scripts/check-bundle-size.mjs exits 0 with "806 chunks within budget". ~105 KB of headroom; the split's own contribution over main's recorded 3201 KB is ~54 KB of module scaffolding, and no ceiling was raised.
E2E (stub ACP backend, offline) main's, and proven so on this tree Job 101125213011: fork.spec.ts:49 expect(more).toBeVisible()element(s) not found, waiting on assistant-more-actions. That is main's #8526, whose fix commit is titled "(E2E fork.spec red on main)". Verified causally rather than argued: with the ported deferral scoped to the slot, playwright/fork.spec.ts passes 3/3 at --retries=0 against the harness gateway + fake ACP backend; changing that one call to an unscoped deferral reproduces the CI failure exactly — same assertion, same locator, same line — and scoping it back makes it pass again.
CodeQL pre-existing on main, line-shifted The annotation is js/insecure-randomness at website/src/pages/ChatPage.tsx:981. main's open alert #639 is the same rule in the same file at :5894, and both lines are the identical sink const sessionId = addDockTerminal(currentProjectRef.current ?? undefined). The tainted source, mintId at website/src/hooks/useBottomTerminal.ts:58, is touched zero times by this diff. Shortening ChatPage.tsx moved the reported line and nothing else. No suppression added, no alert dismissed; Follow-up 1 still owns the real fix.
GPT 5.6 Review real finding, pre-existing markup See the disposition below.
PR Readiness the aggregate of the above Not a target in its own right.

GPT 5.6 Reviewerrors-use-error-notice on ChatPageView.tsx:1082

The finding is correct about the code and wrong about the author, and I am not going to fix it
inside this PR. The three notices it names — uploadError, sidError, pinStatus — exist
byte-identically on origin/main today at website/src/pages/ChatPage.tsx:8672-8688; this diff
moves them into ChatPageView.tsx and changes not one character of them. The lane charges them
because a new file's every line is an added line, which is the structural reason a
behaviour-preserving extraction cannot clear a whole-file rule scan without also paying off the
debt it relocated.

Two reasons that payoff does not belong here:

  1. It is a visible behaviour change, and this PR's contract is that there is none.
    ErrorNotice renders different chrome and, with askAgent, navigates to the chat and unmounts
    the tree that raised the banner. That is a UX change requiring its own screenshot evidence —
    this PR carries a no-visual-delta declaration precisely because it has none to show.
  2. askAgent is a per-surface product judgment the rule itself assigns to the author ("The
    AUTHOR decides per surface"), and an upload failure that happens while a composer holds an
    unsent draft is exactly the case the rule warns can destroy unsaved state. Deciding it in a
    12k-line refactor, unreviewed on its own, is the wrong place for it.

One of the three is also not an error at all by the rule's own definition: pinStatus is
role="status" and reports a pin that succeeded, and the rule explicitly excludes "status text
about something that has not failed".

This needs a human call, and I am deliberately not making it. Either a repository writer
records /ai-review override gpt <head>: pre-existing markup moved verbatim; ErrorNotice migration tracked separately, or the uploadError / sidError migration lands as its own PR with its own
visual evidence and its own askAgent decision — added as Follow-up 5 below. Until one of those
happens this lane stays red on merit, and I would rather report that accurately than clear it by
weakening the gate.

Rebases five and six, and the one red that was mine

main moved twice more while this round was being verified — 45 then 32 commits — and
each time on ChatPage.tsx, so the same semantic resolution ran again. Five more hunks
were re-placed by responsibility:

main change Responsibility Landed in
the mobile drawer's visual-viewport inset (#8529) the page reads the viewport and derives the covered band; the two boxes it insets are the view's pages/ChatPage.tsx + chat/ChatPageView.tsx
capabilities.social_share gating the share entry (#8565) dashboard-config derivation, then the row that renders the entry pages/ChatPage.tsx + chat/useChatPageTranscriptController.tsx
the deep-link deadline waits for the slot list (#8504) session / URL resolution chat/useChatPageSessionController.ts
an offline batch-comment submit bails before switching session (#8348) write-side action, plus the two panel call sites chat/useChatPageActionsController.ts + chat/ChatPageView.tsx
the feature map's Sessions and Subagents rows union of both sides — main's new hoverHold.ts pointer and POST /api/spawn/stop-all alongside this branch's extracted owners docs/feature-map/README.md

The drawer inset is the only one whose placement is a judgement rather than a lookup, and
the reason is worth stating: ChatPageView declares no hooks at all, and the page
already owns the drawer (the mint/spend pair, the phase machine, the POP effect). So the
page reads useVisualViewport() and derives the covered band, and the two reduced numbers
travel to the boxes as layout view-model fields. mobileSessionsDrawerViewport.test.ts
which main added after this branch was last rebased, so CI has never run it against the
split — now reads the owning module per clause: the hook and the Math.max(…)
derivation from pages/ChatPage.tsx, the scrim and panel style clauses from
chat/ChatPageView.tsx. Every regex is unchanged, including the raw-source
key="sessions-backdrop" … z-[46] adjacency guard that keeps
ChatPage.composerChromeOcclusion.test.tsx from silently comparing against undefined.
ChatPage.sid.test.tsx took the union of both sides' cases and both sides' harness
options (connected and slotsLoaded), so main's two slot-list-arrival tests and this
branch's offline-deep-link test all run.

Coverage Gate was real, and it was this PR's

The one red on 96883a23 that was neither main's nor downstream of anything:

1 file(s) below 80% and not baselined — add tests, do not extend the baseline:
     77.3%  src/pages/chat/useChatPageTranscriptController.tsx  (515/666)

Where it came from, measured rather than guessed. I pulled the coverage-frontend
artifact from the last run whose Coverage Gate actually completed on this branch
(33851937200, head 9e5201367) and compared it with the current one: that file measured
463 lines / 83.8% then and 655 lines / 77.4% now. The +192 measured lines arrived
with the THIRD rebase, whose Coverage Gate never ran — it failed closed on main's
poisoned test_slot_close_recreation_race.py with coverage-combine=skipped, so the
per-file check was never reached and the shortfall sat unmeasured for a round. My own
delta to that file is +32/−8 and is fully covered, so the debt is inherited, but it is
this PR's to pay and I have paid it rather than baselining it.

Five direct tests through a real render(<ChatPage />), in the suite that already
drives the page's pin and search seams:

  • a pinned message behind the loaded window pages history and lands the jump on it;
  • it reports unavailable only once the walk has actually run out, not while a page is
    still outstanding;
  • a pending jump is abandoned when the chat it belongs to is left — asserted against
    the same exhausted-walk mock that does reach the notice without the switch, so it is
    not vacuous;
  • a search hit reports its position, which is what arms the travel to it;
  • re-clicking the selected result travels back to it without advancing — two distinct
    code paths behind one click.

That is 40 previously-uncovered lines: 77.4% → 83.51%, check_per_file_coverage.py
exits 0 with 1223 files at or above the floor and none below, and the project average
is 92.87% against the 90% minimum.

One case I wrote and then deleted rather than ship. A sixth test aimed at the
older-fetch rejection arm could not be made deterministic: the transcript's own
older-history pollers page the same offset, and when one of them wins the race the jump's
own dispatch is refused by the thunk's condition, which reads as a supersede and is
deliberately silent — so the arm under test never runs. Three shapes were tried
(reject-always → an unbounded retry loop that OOM-killed the vitest worker; reject-once →
the poller took the failure; reject-N-then-park → same). It is deleted, that arm stays
uncovered, and I would rather say so than leave a test that passes for the wrong reason.

Verification on this head

Everything below run locally on this tree, all exiting 0: tsc -b; eslint src (0
errors); jscpd (0 clones); npm run i18n:check (19 checks · PASS);
lint:phantom-classes; the eleven check_*.py ratchets (black, subprocess_encoding,
changelog_history, focus_cue, testpaths_coverage, builtin_skill_scope,
loop_bound_locks, brand_name, harness_parity, feature_map, per_file_coverage);
flake8 src/kiro_crew test; mypy --platform linux src/kiro_crew (1288 files, no
issues); docs-lint.sh; scrub-lint.sh --no-history (under LC_ALL=C — see the
inherited-failure note above). Plus:

  • the full website vitest suite, not a ChatPage-scoped slice: 1853 files, 29070
    passed, 1 expected fail, 2 skipped;
  • the full offline Playwright suite against a harness gateway with the packaged fake
    ACP backend: 253 passed, well above the suite's own executed-spec floor, 0 skipped;
  • check-bundle-size.mjs on a real analyze build: 807 chunks within budget, App chunk
    3259.1 KB against the 3360 KB ceiling — no ceiling raised.

Follow-ups (deliberately not folded into this refactor)

Recorded here so they are not lost with the PR:

  1. CodeQL alert Dev Fleet restart/sync give no progress feedback, so users fire them twice #639 (js/insecure-randomness, high) is main's, not this PR's. Re-verified on this head: the alert is state: open, created_at: 2026-08-20 (before this branch existed), and its most_recent_instance.ref is refs/heads/main. The tainted source is const mintId = () => Math.random().toString(36).slice(2, 14) at website/src/hooks/useBottomTerminal.ts:58, and this branch's copy of that file is byte-identical to main (the three-dot diff touches it zero times). The extraction only moves the reported sink line by shortening ChatPage.tsx. The one Math.random the diff adds is the sendId expression, which exists byte-verbatim on main at ChatPage.tsx:432 and is a pure move locked by a characterization test. No suppression was added and no alert dismissed. The fix belongs in its own PR: replace mintId with crypto.randomUUID(), which closes Dev Fleet restart/sync give no progress feedback, so users fire them twice #639 for main and for every PR that inherits it.

  2. Unpin lng: 'en' on the two moved notification strings. knowledge_context_was_not_kept and server_did_not_respond are translated into 13 catalogs that no locale can currently render, because both call sites pin the English bytes. That pin is correct here — on main these were bare English literals, so unpinning would change visible language selection and break the byte-preservation contract this PR is — but it is a real wart. The follow-up unpins both and deletes the two assertions that lock the English bytes, as a deliberate behaviour change reviewed on its own.

  3. Shrink the ChatPageView option bag. The view takes whole controllers through a wide props surface. That is the honest shape for a behaviour-preserving strangler step, and narrowing it is a follow-up refactor with its own risk, not part of this move.

  4. Delete two dead re-exports main left in ChatPage.tsx. export { isBrowseCommand } (:14) and export { PREFILL_STORAGE_KEY } from '../utils/navIntent' (:47) have zero importers of any kindgrep -rnE "import \{[^}]*\} from '[^']*(pages/)?ChatPage'" src/ returns nothing, not even a test; every consumer already imports from utils/. Both statements and both of their explanatory comments are byte-identical on origin/main (:17 and :147), and the three-dot diff contains no + line for either, so this is inherited debt rather than something the extraction invented — unlike the six-name re-export block and ChatPageSourceLink, which this diff did create and which were therefore deleted in it. main's own comment claims the second one serves "this page's historical importers"; that claim is false on main today, independent of this branch. The fix belongs in its own PR: two provably-unimportable lines are trivially reviewable on their own evidence, and folding them in here would force-push over four freshly-stamped green lanes to change nothing a user or a compiler can observe.

  5. Migrate the chat pane's two error notices to ErrorNotice. uploadError and sidError render as hand-written bordered <div>s (now chat/ChatPageView.tsx:1082-1093, byte-identical to origin/main's ChatPage.tsx:8672-8688), which the errors-use-error-notice rule in website/AUTOSDE.yaml forbids. The migration is a visible change to chrome plus a per-surface askAgent decision — an upload failure can coincide with an unsent composer draft, which is the state the hand-off destroys — so it needs its own screenshot evidence and its own review, not a hunk inside a behaviour-preserving split. pinStatus, the third box at that site, is role="status" for a pin that succeeded and is outside the rule by its own wording.

Rebase seven — main's registry migration folded in, and the one blocker left

main moved three times under ChatPage.tsx since the last round, and two of those
commits landed in the exact code this split relocates, so the rebase was a real merge
rather than a replay. Base is now a540e334b.

What main contributed, and where each piece now lives

f1f6fb3fa and 73d60a83d (chat-core P5-a / P5-b) replaced the page's inline
renderMessage role chain with registry dispatch, and 7225509f6 reworked every
automatic older-history door. Both landed in code this PR moves, so each hunk was
re-homed onto its new owner rather than replayed onto a page that no longer holds it:

  • useChatPageTranscriptController.tsx now owns the registry host list —
    mergeRenderers([...createTranscriptRenderers(…), stop_event, notice, permission, undrawn, mcp_oauth, hidden_invisible_assistant, bubble]) with resolveRenderer and
    the bubble as the by-reference fallback — because the controller is what owns the
    page's row dispatch. The if-chain, hasReasoningContent / isReasoningRole, and the
    nine row-component imports are gone from it; REASONING_ROLES and NO_AUTO_DENIED
    replace them. It also takes the whole of fix: stop the chat transcript moving on its own #8574: the idle-prefetch effect is deleted,
    sawRealInputRef is replaced by the expiring REAL_GESTURE_AUTH_MS window,
    sentinelPagesSinceInputRef / walkPagesSinceInputRef / walkLastInputAtRef become
    refs with a slot-change reset, the sentinel gains the empty-transcript guard and the
    OLDER_WALK_MAX_PAGES_PER_INPUT bound, keydown / pointerdown join the gesture
    listeners, and every door logs through scrollInspector.
  • ChatPageMessageContent.tsx gains anchorAltIdFor (the lead-message anchor the
    dual-identity restore needs) and loses toolDisclosureKey, which main moved into
    the shared row set — so the disclosure key now has exactly one definition, in
    chat/transcriptRenderers.tsx, for every surface.
  • ChatPageView.tsx takes the restore cover: the centred slotLoading spinner is
    gone, ChatTranscriptSkeleton covers both waits, and the shell's scrollerStyle
    carries visibility: hidden while virt.restoreGate is up.
  • ChatPage.tsx keeps the devWatchMessages probe beside its messages selector.
  • lastErrorIdx is deleted from the actions controller and from the two hand-offs
    that threaded it, because the shared row set derives the newest-error index itself
    (transcriptRenderers.tsx lastErrorIndex).

The six source-contract tests that pinned the old spelling were repointed at the module
that now owns each clause, keeping main's assertion and not the pre-registry one:
chatRolesParity.contract.test.ts (taken from main wholesale, paths repointed),
transcriptRenderers.test.tsx, NoticeCard.test.tsx, invisibleText.test.ts,
ChatPage.mcpOAuth.test.tsx, toolDisclosureKey.collision.test.ts, plus
RecoveryCard.test.tsx whose auto-merge had mixed one side's path with the other's.

Verification on this head

tsc -b --force, eslint src, npm run build, the eleven check_*.py ratchets,
flake8 src/kiro_crew test, mypy --platform linux src/kiro_crew (1296 files, no
issues), docs-lint.sh and scrub-lint.sh --no-history (under LC_ALL=C.UTF-8, CI's
locale — see the inherited-failure note above) all exit 0. The full website vitest
suite: 1865 files, 29308 passed, 1 expected fail, 2 skipped, 0 failed.

The three previously-red lanes the rebase cleared

Backend Tests (3.12, 3), Backend Tests (Windows) (3) and Coverage Gate are all
green on this head with zero Python in the diff. They were main's push-gate
breakage, repaired upstream by c791f0f1d ("hand the push arity scan raw words, not cut
ones"), and the rebase is the whole fix. GPT 5.6 Review is green here too, and the
merge conflict label is cleared (mergeable: true).

Opus 4.8 Review — the lane cannot read a diff this size, and that is the blocker

This is the only item PR Readiness counts as blocking. It is not a finding: the
lane never produced one. Its own steps say why, in order:

  1. Flag oversized reviewable diff prints reviewable changed lines (excluding src/kiro_crew/_vendor): 21290 against its declared MAX_REVIEWABLE_LINES: 15000,
    and warns "likely too large for one reliable review pass. Consider splitting it into
    smaller, self-contained PRs."
  2. Prefetch the reviewable diff writes 1,335,611 bytes to one file, and the
    discovery prompt's only instruction for obtaining the diff is to Read that file.
  3. Discovery is given --allowedTools "Read,Grep,Glob" and a 200,000-token context
    window. It cannot read 1.34 MB and has no shell to chunk it, so it ends
    subtype: success, is_error: false, num_turns: 16,
    permission_denials_count: 36without emitting [OPUS-DISCOVERY].
  4. Capture discovery candidates then fails closed on the missing marker, correctly:
    "validation against a truncated or empty list would emit a clean [OPUS-REVIEWED]
    verdict and pass the gate on a review that never happened."

Four attempts across two heads (745a3f1d0 ×3, 2c3df4a32 ×1) are byte-for-byte the
same outcome, and the rebase made the diff larger, not smaller. Nothing on this branch
can change that: the lever is either the workflow (out of this PR's scope, and behind the
workflow-change guard) or the diff size.

Recommended: split this PR. One controller per PR, extracted from the then-current
ChatPage.tsx, in dependency order — ChatPageMessageContent (~1.5k changed lines, and
first because every controller imports it), then useChatPageSessionController (~1.4k),
useChatPageResourcesController (~1.7k), useChatPageComposerController (~3.5k),
useChatPageActionsController (~3.8k), useChatPageTranscriptController (~4.6k, which
carries the registry host list and the source-contract repoints), and
ChatPageView (~4.5k) last, once the page is only composition. Every slice is then well
inside the lane's own 15,000-line budget and reviewable on its own evidence. A
two-way split (the six extracted modules ≈12k, then the view plus the final page
reduction ≈9k) also clears the threshold, but each half is still a large read.

The other route is a maintainer /ai-review override on this head, which is a
repository-writer judgment and not mine to record. Worth weighing either way: every other
AI lane — GPT 5.6 Review, Design Review, UX Review, First Principles Review,
Code Review — is green on 2c3df4a32, so an override skips one lane rather than the
review as a whole.

CodeQL — red in the UI, and not a readiness blocker

Unchanged from follow-up 1 below, re-verified on this head. Alert #639
(js/insecure-randomness, high) is state: open on refs/heads/main at
a540e334b — this PR's own base — with the same alert number, the same rule, and the
same tainted source, const mintId = () => Math.random().toString(36).slice(2, 14) at
website/src/hooks/useBottomTerminal.ts:58, a file this diff touches zero times. The
reported sink is const sessionId = addDockTerminal(…), byte-identical to main's; only
its line number moved, from ChatPage.tsx:5959 on main to :1017 here, because the
file shrank. The check's own summary says the rest: "Alerts not introduced by this pull
request might have been detected because the code changes were too large."

It also does not gate the merge. PR Readiness reads the dynamic/github-code-scanning/codeql
workflow run, whose conclusion on this head is success; the red CodeQL check-run
is the advanced-security alert report, which readiness does not consume. Hence
1 blocking readiness item(s), and that one is the review lane above. No alert was
dismissed and no suppression added.

Related Issues

None — planned behavior-preserving refactor.

Checklist

  • One Conventional Commit
  • Rebased on current origin/main
  • Tests and owning specifications updated
  • No merge or auto-merge requested

@bolichen97
bolichen97 requested a review from a team August 31, 2026 11:07
@bolichen97
bolichen97 requested a review from a team as a code owner August 31, 2026 11:07
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 31, 2026
@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch from aa12b26 to ce52c46 Compare August 31, 2026 11:09
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 2c3df4a3241047c7ea8947a499e2761d31916404 and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 2c3df4a

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 2c3df4a3241047c7ea8947a499e2761d31916404: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

UX-level review of 2c3df4a3241047c7ea8947a499e2761d31916404 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

The diff stat showed no image files, so no screenshots to review. I have everything needed: this is a behavior-preserving refactor; the only two new i18n keys are extractions of strings that existed verbatim in the base, translated into all 12 locales but deliberately pinned to English at both call sites to keep legacy notification bytes.

UX-Verdict: PASS

Pure code-motion refactor; every user-facing string and behavior is preserved byte-for-byte, so there is no new experience to break.

Suggestions

  • useChatPageComposerController.tsx:88 and useChatPageActionsController.ts:673 pin lng: 'en', so the 12 translations this PR adds for server_did_not_respond and knowledge_context_was_not_kept never render — a German user reads a localized "message saved as draft" notice with an English suffix. Once the legacy-bytes compatibility window closes, drop the two pins so the already-shipped translations take effect.

[UX-REVIEWED] 2c3df4a

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5) — ✅ PASS

Premise-level review of 2c3df4a3241047c7ea8947a499e2761d31916404 — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All checks converge: shared helpers defined once (3 production consumers, no duplication), the feature-map claim verified, i18n entries gate-forced, and the two riskiest spots (string localization, facade removal) handled conservatively and declared. Final review:

First-Principles-Verdict: PASS

An 8k-line page becomes seven owned modules; every rider is a deletion, a CI-gate mandate, or a declared conservative choice — users learn nothing new.

What this change ships

Intent: make the chat page maintainable by giving its interleaved concerns explicit owners, with zero behavior change — a MOVE (refactor), neither fix nor addition. The harm is measured, not asserted: three unrelated behaviours landed on ChatPage.tsx while this PR was open, and the scroll-shell mutation gate already carried "post-extraction" scaffolding anticipating this split.

  1. Chat page split into session/composer/actions/resources/transcript/message-content/view owners; route, props, wire formats unchanged — justified
  2. Two send-failure notices now read from the catalog, pinned to legacy English bytes (lng: 'en', both call sites) — justified, i18n added-lines gate
  3. Twelve locales gain those two entries, unreachable while pinned — declared; forced by the missing-key gate
  4. Moved helpers no longer importable from the page module — justified deletion
  5. Spec, RFC, and feature-map pointers repointed (verified: api.createWorktree has exactly 1 production hit, useChatPageActionsController.ts:1260) — mandated spec-sync
  6. New characterization/coverage tests on the new module boundaries — mandated per-file coverage floor
  7. Scroll-shell mutation gate now also mutates ChatPageView.tsx — undeclared in the (truncated) description; derived, keeps the gate meaningful
  8. Earlier 5s waitFor widening reverted after measurement — justified deletion
  9. Extraction-narration comments restated as invariants; dead switchStability port deleted — mandated Comments rule
  10. shouldPaginateOlder whitespace reformat — undeclared cosmetic rider, zero cost

Counts run: controllers have 1 production consumer each (inherent to a split, not generalization); uniqueNotificationTs/createFailReason defined once, 3 production consumers, no sibling duplicate; jscpd's 0-clone claim consistent with my greps. Items 7 and 10 are the only undeclared items and neither would change what ships.

[FIRST-PRINCIPLES-REVIEWED] 2c3df4a

@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Design-level review of 2c3df4a3241047c7ea8947a499e2761d31916404 — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: PASS

A real monolith problem, solved by the honest strangler shape: ownership boundaries with behavior byte-preserved, and every trade-off named, tested, or tracked as a follow-up.

Suggestions

  • The composer→actions controller ordering is a runtime invariant tsc/eslint cannot see, currently guarded only by the "Do not reorder these two controllers" comment in ChatPage.tsx; pin it with a cheap source-contract test (the repo's established readFileSync pattern) so a future reorder fails a test instead of a voice/dropdown interaction.

[DESIGN-REVIEWED] 2c3df4a

@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch 3 times, most recently from 412d33d to 5f0de88 Compare August 31, 2026 11:36
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 96883a23afd54931ba3f81fd5aea9b394d6d4e04 — this comment is updated in place on each push.

Review details

No findings.

[OPUS-REVIEWED] 96883a2

Verdict parsed from the review's SHA-scoped output markers for commit 96883a23afd54931ba3f81fd5aea9b394d6d4e04.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 96883a23afd54931ba3f81fd5aea9b394d6d4e04: <one-sentence reason>

@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch from 5f0de88 to 3cb0475 Compare August 31, 2026 11:52
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 31, 2026
@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch from 3cb0475 to 6a6d875 Compare August 31, 2026 19:50
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: action required A blocking check or review needs attention labels Aug 31, 2026
@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch from 6a6d875 to aece56d Compare August 31, 2026 20:38
@bolichen97

Copy link
Copy Markdown
Collaborator Author

Follow-up on aece56d2a0ce30abcf307ed4f3894ded83794b0b:

  • The composer-to-actions forward-ref ordering is now documented at both the nullable sendRef declaration and the page composition call site. The controllers remain one page-only composition boundary; ChatPageView documents why it is the sole whole-controller consumer in this behavior-preserving strangler step.
  • The historical ChatPage named-export facade remains intentionally for downstream import compatibility. Repository tests now import the owning ChatPageMessageContent module directly, so the facade is no longer a test-only shortcut; removing it would be an unnecessary public import-surface break in a behavior-preserving refactor.
  • sendId now keeps its original s-<base36 timestamp>-<6 base36 nonce> contract and collision space while deriving the nonce from the existing CSPRNG helper. A direct characterization test asserts both the secure source and exact opaque wire shape.
  • Stale RFC line citations now use the extracted owners' stable symbols, and docs lint passes.
  • The two i18n call sites remain pinned to English deliberately to preserve the exact pre-refactor user-visible bytes; unpinning them is outside this no-behavior-change PR.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Follow-up on the current First Principles review for aece56d2a0ce30abcf307ed4f3894ded83794b0b:

  • Named-export facade: retained deliberately as a source-compatibility boundary. A repository-wide zero-import result cannot prove zero external consumers; removing public exports in this behavior-preserving extraction would be a breaking change. Tests now target ChatPageMessageContent directly, so the facade is not retained merely for tests and has no runtime/UI effect.
  • English notification bytes: the pre-extraction page emitted these legacy English strings rather than locale-selected content. The explicit lng: 'en' pins preserve that exact existing rendered behavior. Removing them would be a user-visible localization/product change, so it is intentionally outside this no-visual-delta refactor; the catalog entries keep the future localization seam ready.
  • sendId entropy: the moved ChatPage path keeps the exact s-<base36 timestamp>-<6 base36 nonce> wire shape and collision space while using the existing CSPRNG helper, with a direct characterization test. ChatPane.tsx is a separate, unmodified code path outside this PR's authorized ChatPage ownership; widening this PR into a cross-page randomness sweep would violate its scoped behavior-preserving mandate.

These are deliberate scope/compatibility decisions, not unresolved defects in the extracted ChatPage surface.

@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch from aece56d to be635e0 Compare August 31, 2026 21:37
@bolichen97

Copy link
Copy Markdown
Collaborator Author

CodeQL follow-up for current head be635e0ec11c74797ab266aeadbe8728e5b28fbe:

  • The failing result is existing open alert Dev Fleet restart/sync give no progress feedback, so users fire them twice #639. Its instance is already present on the current PR base 283625e16efbd32a5cc6655ed9a2974f45a36241 at the original ChatPage.tsx:5178 call site.
  • This extraction moves that same addDockTerminal(...) call to ChatPage.tsx:942; git diff origin/main...HEAD -- website/src/hooks/useBottomTerminal.ts is empty.
  • The actual weak source is the existing terminal-session ID mint in useBottomTerminal.ts, outside this PR's explicitly authorized ChatPage-only file ownership. I will not suppress or disguise the alert at the call site.

A real remediation is a small, separately scoped change to that hook (with a direct terminal-ID test); it requires explicit scope authorization rather than being folded silently into this behavior-preserving refactor.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Follow-up on the current be635e0ec11c74797ab266aeadbe8728e5b28fbe First Principles advisory:

  1. The two calls deliberately pin the pre-refactor English bytes. The old page emitted those strings as English literals, so unpinning would change visible language selection. The localized catalog entries are the structural i18n seam required by the extraction; activating them is a product/localization follow-up, not a behavior-preserving refactor edit.
  2. sendId retains the exact opaque wire contract (s-<base36 timestamp>-<6 base36 nonce>), nonce width/collision space, and all send/stream timing. Replacing the random source with the repository CSPRNG helper does not change any user-visible or protocol shape; the direct test locks both the secure source and output shape. The broader ChatPane path is a separately owned surface and is not silently bundled into this PR.
  3. The five-symbol facade remains because zero in-repository importers cannot establish zero downstream importers. Removing it would narrow a historical public source surface during a declared compatibility refactor. Tests now import the extracted owner directly, so the facade is no longer a test-only coupling and has no runtime or rendered cost.

The extraction itself, its docs, and its characterization coverage remain the smallest behavior-preserving change; none of the suggested deletions is appropriate in this PR.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 31, 2026
@bolichen97
bolichen97 force-pushed the refactor/chat-page-boundaries branch from be635e0 to c93f022 Compare August 31, 2026 23:25
@bolichen97

Copy link
Copy Markdown
Collaborator Author

Coverage Gate follow-up for current head c93f0229c26f03caefd1b97d8848d51b3a3b0e1d:

  • Added one direct ChatPageMoreCoverage characterization for the existing foreground kirocrew-tool-call / playwright-cli Browser-panel auto-open behavior.
  • It covers the previously cold listener path without changing production code, CI configuration, or the coverage baseline.
  • Local full ChatPage coverage now passes 55 files / 489 tests and reports ChatPage.tsx at 454/556 covered lines (81.65%); the failed gate required 80% and was short by three lines.

The new head is rebased onto current main; CI was restarted by the force-push.

@github-actions github-actions Bot removed the readiness: action required A blocking check or review needs attention label Aug 31, 2026
buluoray
buluoray previously approved these changes Sep 3, 2026

@buluoray buluoray left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of be070c3 — refactor(chat): split chat page controllers

Reviewed as a behaviour-preserving controller split. I did not attempt a line-by-line pass of all 11k lines; I separated moved code from rewritten code first, then reviewed only the rewritten surface and the structural invariants a controller split can break.

Move vs rewrite

The pre-split ChatPage.tsx was 8909 lines; it is now 2099 lines plus seven extracted modules (~11.5k total). Of ~6576 substantive (non-import/non-trivial) lines in the eight head files, ~4192 (~64%) are byte-identical (modulo indentation) to the base monolith. Per-file verbatim-move share: ChatPageMessageContent 95%, Composer 78%, Transcript 70%, ChatPage 65%, Session 65%, Resources 65%, View 59%, Actions 41%. Of the remaining ~36%, roughly 70% is mechanical controller boilerplate — Pick<> port typedefs, prop-bag destructuring, return objects, and JSX prop-threading (foo={ctrl.foo}). The genuinely rewritten logic surface is small and concentrated in useChatPageActionsController.ts (the send callback with references re-plumbed through prop bags) and the View's JSX rethreading.

Defect-class checks (the ones a controller split introduces)

  • Duplicated state / two sources of truth: none. No useState/useRef/useReducer name is declared in more than one controller; the ports/Pick<> pattern keeps single ownership.
  • Effect cleanup lost: none. useEffect count 96/96, return () => cleanups 28/28 (base vs head).
  • Listener registered twice or never: none. addEventListener 18/18, removeEventListener 17/17, and the per-event-name multiset is identical. The 18-add/17-remove asymmetry is pre-existing in base, not introduced here. setTimeout/clearTimeout/requestAnimationFrame/ResizeObserver/MutationObserver/.subscribe counts all identical.
  • Mid-turn steer / turn-boundary reorder: preserved. The steer decision block (outcome === 'accepted' && steerNow && ... && !body.queued && !body.steered) is identical apart from _busy being renamed busyAtSend. The one new cross-module ordering invariant — syncSlotRunningFromServer dispatch must be registered before the actions controller's auto-send effect that reads slotRunning via a live store.getState() — is explicitly pinned by the new ChatPage.runningReconcileOrder.test.ts and holds on head (base ordering 4320→4906 preserved as 603→609). Frontend tests are green.
  • Ref/callback identity churn: the load-bearing optimistic-bubble read _busy = selectComposerBusy(store.getState(), slot ?? null) was already a live store read in base (line 4703); head renames it busyAtSend and reuses it in both the bubble gate and the steer gate. Pure rename, same value.
  • Guard / refusal survival: all early-return refusals survive. outcome === 'refused' 1/1, restoreComposerAfterFailedSend 3/3, if (!raw 2/2, sendingRef.current 4/4, disabled 19/19. Elevated counts for sttDisarmedRef/frozenInputRef/postStopEditedRef are port-plumbing (the Pick<> unions plus receiving-controller destructuring add references), i.e. the refs are threaded across controllers, not dropped.
  • Externally observable behaviour: the default export signature ChatPage({ mode, embedded, embedMode, popout, noUrlSync }) is byte-identical (same props, same defaults). All production consumers (App.tsx, ChatPanel.tsx, CoAuthorPanel.tsx, ArtifactChatPanel.tsx) use the default import; the removed named re-exports had zero production consumers.

CodeQL (human-flagged)

The failing CodeQL check is js/insecure-randomness on the relocated mintSendId, which is byte-identical to base (s-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}). The Math.random() sink is pre-existing and merely moved; the value is a client-side correlation id for optimistic bubbles, not a security token. Not introduced by this PR and not a reachable security hole, so not blocking under the blocking criteria.

Verdict

0 blocking findings. No reachable security hole, crash/data-loss, user-visible correctness defect on the chat path, removed guard, or AUTOSDE blocking-rule violation introduced by this diff.

Non-blocking (advisory, already dispositioned): the actions controller consuming a large (~45-field) composer prop bag is structural coupling, not a behaviour defect; the 12 shipped i18n translations pinned to { lng: 'en' } preserve the pre-refactor English bytes by design and are a deferred localization follow-up.

What I could not verify

I did not diff every one of the ~4200 moved lines individually — I relied on structural invariants (effect/listener/timer/state/guard counts and the key decision blocks) rather than statement-by-statement equality, so a subtle reference change inside a line my matcher counted as identical is theoretically possible but unlikely given every structural check held. I did not run the suite locally; I relied on the green CI (frontend tests, E2E, coverage) and on the UX/Design lanes' byte-identical verification for pixel-level render equivalence of the View's relocated JSX.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Head moved to 34a50faefab6c6a408b7ad4c3bce294f82135789 (rebase onto 1b484549d plus one doc-line fix).

Recording what changed since f12a4889b so the fresh lane stamps are easy to reconcile. Two things, and only two.

1. Re-rebased onto the current tip. origin/main had advanced three commits (4fc58f5e6 cloud, 997d59421 babysit, 1b484549d OAuth entropy scope). None of them touch website/, and the rebase applied with zero conflicts. The three-dot diff keeps exactly the shape it had before -- 83 files, +11239/-7605 -- so no semantic re-placement was needed this round.

2. Fixed a stale source pointer on a row this PR itself edits. website/docs/theming-contract.md:170 is the third-party theming contract's map from a stable class hook to the file that carries it. The row now reads:

| `chat-container` | the chat scroll region (`ChatPane.tsx`, `pages/chat/TranscriptScrollShell.tsx`) |

Why it was wrong: at the original merge base the chat-container div lived in pages/ChatPage.tsx, so this PR retargeted the row to pages/chat/ChatPageView.tsx when it moved the div there. Main's later extraction (0ee0da3e0) then moved the div out again into pages/chat/TranscriptScrollShell.tsx. Accepting that extraction on rebase -- which is correct -- left the row naming a file with zero chat-container occurrences. grep -rn chat-container website/src at this head finds the class only at components/ChatPane.tsx:701 and pages/chat/TranscriptScrollShell.tsx:60; ChatPageView.tsx has none, not even in a comment.

Worth being straight about the provenance: the row was already stale on main for the same reason -- it names ChatPage.tsx, which has zero occurrences there either -- so this is a strict improvement, not a regression repair. Nothing catches it. No doc-to-code gate exists, Docs Lint validates only indexes and links, and no consumer parses the doc's file paths (themes.md, website/scripts/check-theme-colors.mjs and src/test/DisplayPanel.test.tsx cite the doc but carry no file pointer of their own). The asymmetry was visible in-tree: TranscriptScrollShell.tsx:59 carries // stable theming hook 'chat-container' -- see website/docs/theming-contract.md while the doc pointed back at the wrong file. The fix names the file that carries the class, matching every sibling row (sidebar to ChatSidebar.tsx, input-area to ChatInput.tsx).

I audited the class rather than the instance, and this is the only occurrence: the other 16 source paths this PR's doc changes add all resolve and are factually correct.

Gates re-run green at this head locally: npx tsc -b; npx eslint src/ --max-warnings 0 (the ci.yml ceiling, clean without any disable); npx jscpd .; npm run i18n:check and npm run lint:phantom-classes with *_BASE_REF=origin/main; scripts/docs-lint.sh; the feature-map and brand-name gates; and 89 vitest spec files -- every changed spec plus every ChatPage* spec -- at 913 passed.

Unchanged from the previous head: the CodeQL red is still open alert #639 js/insecure-randomness, whose tainted source is website/src/hooks/useBottomTerminal.ts:58, a file with zero hits in this branch's three-dot diff and simultaneously instanced on refs/heads/main. The existing rebuttal and the crypto.randomUUID() own-PR follow-up stand as recorded; I have not duplicated that record.

@bolichen97

Copy link
Copy Markdown
Collaborator Author
  • Subtraction — delete export { isBrowseCommand } (ChatPage.tsx:14) and export { PREFILL_STORAGE_KEY } from '../utils/navIntent' (ChatPage.tsx:47)rebutted (recorded as a follow-up for its own PR).

grepped import .*from '.*ChatPage' for both names: 0 non-test importers each; every consumer already imports from utils/.

Your dead-code finding is correct and I reproduced it exactly. grep -rnE "import \{[^}]*\} from '[^']*(pages/)?ChatPage'" src/ returns zero hits of any kind — not even a test importer. Every isBrowseCommand consumer imports from utils/browseCommand, and all six PREFILL_STORAGE_KEY consumers (two controllers, four tests) import from utils/navIntent. Both re-exports are genuinely unreachable.

Why I am still not deleting them here: unlike the two you found before, these are not this PR's code. That distinction is the whole disposition, so here is the proof rather than an assertion. Both statements, and both of their explanatory comments, are byte-identical on origin/mainexport { isBrowseCommand } at main's ChatPage.tsx:17 under the same three-line "so the symbol ... stays importable from here" comment, and export { PREFILL_STORAGE_KEY } at main's :147 under the same "re-exported here for this page's historical importers" comment. git diff origin/main...HEAD -- website/src/pages/ChatPage.tsx contains no + line for either export; the only +/- pair touching those names is the isBrowseCommand import moving line 18 to 20 as the file shrank. This PR carried both verbatim through three semantic rebases.

The framing they are "the two surviving siblings of the facade class this PR removed" is what I am pushing back on, and it matters for scope. The class you closed twice was real but narrower than it looks: the six-name re-export block and export type ChatPageSourceLink were both invented by this diff — dead compatibility shims the extraction created for a compatibility requirement that does not exist, which is exactly why deleting them was in scope and why I fixed both without argument. These two are inherited debt in a file this PR is shrinking. They predate the extraction, and main's own comment on the second one asserts a reason ("this page's historical importers") that your grep and mine both show is false — on main, today, independent of this branch. Deleting them is a correct cleanup of main, not a correction of this PR.

The cost of folding it in is specific rather than theoretical, which is why the answer is a separate PR and not "later". This head has four lanes freshly stamped green at 34a50faefab6c6a408b7ad4c3bce294f82135789 — GPT 5.6, Design, UX, and your own lane, which produced a real PASS here after returning no verdict on the previous head — and the Opus 4.8 lane is mid-re-run. A two-line amend force-pushes over all of that and buys another full CI round on an 83-file diff, to change nothing a user or a compiler can observe. Against that, a standalone PR deleting two provably-unimportable lines from main is trivially reviewable on its own evidence, and it does not have to be argued through this diff's 11k lines of code motion.

I am recording it in the PR body's follow-up list next to the crypto.randomUUID() item, which is the same shape: a real defect this branch demonstrably did not introduce, kept out of a behaviour-preserving split so the split stays reviewable as one. If a maintainer would rather have the two lines gone now than have this head's stamps preserved, say so and I will amend — the fix itself is uncontroversial, only its placement is.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Open PR relationship audit

This is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion.

Relationship findings

  • PR #6825 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #6825: MERGE_DISCUSSION. A behaviour change and a wholesale relocation of the same ~200 lines. Sequencing this (fix first, refactor rebases) is far cheaper than the reverse, and should be agreed rather than discovered at merge time. Files: website/src/pages/ChatPage.tsx.
  • PR #7163 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7163: REBASE. Ordering-sensitive but complementary: a chat-page decomposition versus a new transcript row. Whichever merges second must relocate the section_marker branch, its import, and the three ChatPage.tsx source assertions to useChatPageTranscriptController.tsx. Neither should be closed. Files: website/src/pages/ChatPage.tsx, website/src/test/chatRolesParity.contract.test.ts, website/src/test/SectionMarkerRow.test.tsx.
  • This PR is OVERLAPPING with PR #5591. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7255: REBASE. Behaviour fix inside the relocated send path; needs a sequencing decision, not closure of either PR. Files: website/src/pages/ChatPage.tsx.
  • This PR is OVERLAPPING with PR #7336. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7255: REBASE. 7336 threads a fifth draft store through exactly the ref/effect cluster 7255 relocates; the two diffs cannot both apply to ChatPage.tsx. Files: website/src/pages/ChatPage.tsx.
  • This PR is OVERLAPPING with PR #7693. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7255: REBASE. Cross-referenced on 7255's timeline (2026-09-01). Direct conflict over the composer's agent/model bindings; ordering must be agreed. Files: website/src/pages/ChatPage.tsx.
  • This PR is OVERLAPPING with PR #7916. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7255: REBASE. Highest-conflict open pair: 94 files and +6420 of new ChatPage.tsx behaviour against a PR that empties that file, plus 9 shared test files. Neither can absorb the other; a landing order has to be agreed, and whichever loses re-places its hunks by ownership. Files: website/src/pages/ChatPage.tsx, website/src/test/ChatPage.scrollShell.recipe.test.tsx.
  • This PR is OVERLAPPING with PR #8040. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7255: REBASE. Already-landed change inside a region the primary relocates; it is why the head currently conflicts. Files: website/src/pages/ChatPage.tsx.
  • PR #7616 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7616: KEEP. Independent files and independent goals; the interaction is a stale comment reference at worst. Files: website/src/pages/chat/useChatPageComposerController.tsx.
  • PR #7760 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #7760: KEEP. A large file-split PR moves the exact line PR #7760 edits; ordering must be agreed and the loser rebased, since a clean textual merge can silently discard the fix. Files: website/src/pages/ChatPage.tsx.
  • PR #8261 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #8261: KEEP. Neither PR duplicates the other, but they contend for one line in ChatPage.tsx and the loser silently reintroduces the '/settings/voice' literal 8261 exists to remove. The authors should agree on landing order and, if 7255 lands first, 8261 should re-target the line in website/src/pages/chat/ChatPageView.tsx. Files: website/src/pages/ChatPage.tsx.
  • PR #8289 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #8289: KEEP. Complementary, not competing: PR #7255 changes no behavior and does not fix Issue #8204. Keep both; sequence them and rebase the loser of the race. Files: website/src/pages/ChatPage.tsx.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

UX CONCERNS — lng: 'en' on knowledge_context_was_not_kept and server_did_not_respond: rebutted (out of scope for a byte-preserving refactor).

A behavior-preserving refactor ships 12 new translations for two strings it then hard-pins to English, so non-English users get spliced-language failure notices. Fix: drop lng: 'en' from both calls.

The lng: 'en' pin is deliberate and load-bearing for this PR's mandate, not an oversight. Both call sites carry the rationale in-code: the fragment is interpolated into a larger localized sentence, and the split must "keep the exact legacy notification bytes" / "a locale switch must not change either the language or the casing of a rejected send's reason." This PR is a strangler-step MOVE whose whole contract is byte-identical behavior — dropping lng: 'en' would change the rendered notification for every non-English user, which is precisely the behavior delta a byte-preserving refactor must not introduce. The 12 locale values were added only because the i18n extraction gate requires a key added to en.json to exist in every catalog; they are intentionally inert until a follow-up decides to un-pin.

Disposition: rebutted on proportionality — the reviewer's fix is a genuine UX improvement but it is a behavior change, so it belongs in a dedicated follow-up (un-pin knowledge_context_was_not_kept / server_did_not_respond and drop the manual charAt(0).toLowerCase()), not in this split. Verified the lng: 'en' calls do not exist on the base (grep -c "lng: 'en'" on ChatPage.tsx@base = 0 for these two keys), so this is not a pre-existing defect either — it is the faithful relocation of the byte-preservation intent. No code change on this head.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

First Principles CONCERNS — leave isBrowseCommand / PREFILL_STORAGE_KEY page re-exports standing: rebutted (pre-existing, out of scope).

The facade-removal argument kills three re-exports; this PR deletes one and leaves two siblings standing. Subtraction: delete export { isBrowseCommand } and export { PREFILL_STORAGE_KEY }, then drop the PREFILL_STORAGE_KEY entries from the eight ChatPage mocks.

Both surviving re-exports pre-exist on main and this PR does not touch them: on the merge-base ChatPage.tsx they sit at lines 17 (export { isBrowseCommand }) and 147 (export { PREFILL_STORAGE_KEY }), and in this PR's own diff both lines appear as unchanged context (leading space, not +). The one facade this PR deletes is a facade the PR itself created and then removed during the move of its owning helper — deleting it is part of the move. The two siblings are unrelated standing exports the split neither introduced nor relocated.

The asymmetry the reviewer flags is real but the fix is disproportional to a behavior-preserving split and not this PR's to make: (1) PREFILL_STORAGE_KEY is not zero-consumer in effect — eight ArtifactDetailPage* specs mock ../pages/ChatPage with a PREFILL_STORAGE_KEY field, so removing the re-export changes the shape those mocks assert against; touching them widens this PR into the artifact-page test surface for no behavior gain. (2) Removing a pre-existing public re-export is a subtraction with its own review, independent of the chat-page split.

Disposition: rebutted on scope/proportionality — the cleanup is welcome as a dedicated follow-up (delete both pre-existing re-exports and reconcile the eight artifact-page mocks in one focused PR), not folded into a byte-preserving refactor. No code change on this head.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

CodeQL js/insecure-randomness (HIGH), alert #639, website/src/pages/ChatPage.tsx:965: rebutted — not a security use, and pre-existing on main, not introduced by this PR.

This uses a cryptographically insecure random number generated at Math.random() in a security context.

I am pushing back on the security-context classification, not dismissing the scanner. Evidence:

1. It is pre-existing on main, not introduced here. Alert #639 was created 2026-08-20 and its most_recent_instance is pinned to ref: refs/heads/main, commit 9d3d279, ChatPage.tsx:5473 — i.e. it is an OPEN alert on the base branch that predates this PR by two weeks. CodeQL's own comment on this run says "alerts not introduced by this PR may surface because the diff is large," which is exactly this: the flagged merge-ref line 965 is a dataflow artifact of an 11k-line move-dominant diff, not a new sink. The Math.random() it traces lives on main at ChatPage.tsx:435 (return \s-${Date.now()...}-${Math.random().toString(36).slice(2, 8)}``).

2. This PR only RELOCATED that byte-identical code — and not even in a file this diff touches. The value CodeQL flags is the optimistic-send correlation id sendId. In this PR it lives at website/src/components/ChatPane.tsx:483, byte-identical to main's expression, and ChatPane.tsx is not in this PR's diff (git diff --name-only origin/main...HEAD -- website/src/components/ChatPane.tsx is empty). The chat-page split neither introduced nor changed this generator.

3. It is not a security context. sendId is a purely client-side UI correlation id: it tags an optimistic user bubble so the server's echo of that same message reconciles against the already-rendered bubble instead of appending a second one. Its own code comment states this: "the content-equality fallback can never reconcile the server echo against the optimistic bubble — without this id the echo appends a SECOND user bubble." It is never a token, nonce, session id, CSRF value, or anything that gates an auth or access decision. Math.random() collision-resistance for a per-send UI key is entirely adequate; js/insecure-randomness over-classifies the downstream string comparison as security-sensitive.

Disposition: rebutted — not a reachable security use (cosmetic UI de-dup id), and pre-existing on main rather than introduced by this behavior-preserving refactor. Hardening the pre-existing sendId/reqId generators to crypto.randomUUID() is a legitimate standalone follow-up on main's owner of that code, not an in-scope change for a byte-preserving split (which must not alter unrelated code). No code change on this head. The required PR Readiness check is success; this CodeQL alert is not the required merge gate.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

GPT 5.6 BLOCKING — errors-use-error-notice, website/src/pages/chat/ChatPageView.tsx:1082not fixed here, and deliberately so.

Upload, session, pin, or action failure -> these render paths omit ErrorNotice or its required hand-off decision -> users lose structured recovery context.
Fix: Render error states through ErrorNotice; add askAgent or a concrete No hand-off draft-protection comment to every touched notice.

The finding is correct about the code. It is wrong about who wrote it, and the remedy it prescribes cannot land in this PR without breaking the contract this PR exists to keep.

The markup is main's, verbatim. All three boxes at that site — uploadError, sidError, pinStatus — exist byte-identically on origin/main today at website/src/pages/ChatPage.tsx:8672-8688. Check it against this branch's chat/ChatPageView.tsx:1082-1097: same class list, same inline borderColor: color-mix(...), same aria-label keys (pages.chatPage.dismiss_upload_error, pages.chatPage.dismiss_error, app.dismiss), same &times; glyph. The three-dot diff changes not one character inside them; it only changes which file they live in. The lane charges them because every line of a NEW file is an added line — which is the structural reason a behaviour-preserving extraction cannot clear a whole-file rule scan without also paying off the debt it relocated. That is a real observation about the gate, not a complaint: it is exactly why the fix has to be its own change.

Why the migration does not belong in this diff.

  1. It is a visible behaviour change, and this PR's declared contract is that there is none. ErrorNotice renders different chrome, and with askAgent on it navigates to the chat and unmounts the tree that raised the banner — the rule's own doc says so. This PR carries a no-visual-delta declaration and a Screenshot Evidence pass precisely because it has no rendered change to show. Folding in a new banner would make that declaration false, and I am not going to fabricate visual evidence for a surface I cannot exercise here.
  2. askAgent is a per-surface product judgment the rule assigns to the author — "The AUTHOR decides per surface", website/AUTOSDE.yaml. And this surface is the awkward one: an upload failure is raised while the composer holds an unsent draft, which is the exact state the rule warns the hand-off destroys. That decision deserves its own review, not a hunk buried in a 12k-line move.
  3. One of the three is not an error by the rule's own definition. pinStatus is role="status" and reports a pin that succeeded. The rule text excludes "status text about something that has not failed", so dressing it as an error would be a regression against the same rule that flagged it.

What I did instead. Recorded it as Follow-up 5 in the PR body, with the concrete line ranges on both sides, the two-of-three scoping, and the askAgent question stated so whoever takes it does not have to rediscover it. No suppression was added, no rule was edited, and website/AUTOSDE.yaml is untouched by this branch.

This needs a human call and I am not making it. Two honest ways forward:

  • a repository writer records /ai-review override gpt 96883a23afd54931ba3f81fd5aea9b394d6d4e04: pre-existing markup moved verbatim; ErrorNotice migration tracked as follow-up 5, or
  • the uploadError / sidError migration lands first as its own PR with its own screenshots and its own askAgent decision, and this one rebases on top of it.

Until one of those happens this lane stays red, and I would rather report that accurately than clear it by weakening the gate or by smuggling a UX change through a refactor.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Round on 745a3f1d0 — where the four reds are, with the evidence for each

Every frontend lane is green on this head, including the two that were mine last round:
Frontend Tests (1-4), Frontend Coverage Merge, Frontend Lint & Type Check,
Bundle Size Gate, E2E (stub ACP backend, offline), and all four Backend Tests (Windows)
except shard 3. Four checks are red. Three are main's and one is the reviewer lane's own
plumbing; none is a finding about this diff.

Backend Tests (3.12, 3) and Backend Tests (Windows) (3)main's, and reproducible

Both name the same file, and only that file:

FAILED test/test_push_branch_gate.py::TestUnrecognisedOptionsReadProtectively::test_extglob_patterns_are_wildcards_too
  - AssertionError: 'git push origin @(main)' was not read as a wildcard shape
FAILED …::test_a_glued_all_branches_flag_keeps_its_identity
  - AssertionError: assert 'git-publish-push-bare' in frozenset()
FAILED …::test_a_background_operator_reads_protectively
FAILED …::test_a_heredoc_strip_operator_consumes_its_delimiter
FAILED …::test_a_glued_redirection_keeps_the_precise_positional_identity
FAILED …::test_every_bash_metacharacter_is_accounted_for

That class arrived on main two hours before this run, in eaa8a45bb
"fix(security): model publish option arity so the floor tag holds (#7808)" (2026-09-05
00:03:28 -0700). Attribution is not an inference here — it is checkable three ways:

  1. This diff contains no Python at all. git diff --name-only origin/main...HEAD is 92
    files: website/** plus twelve docs/** markdown files. Nothing under src/kiro_crew/
    or test/.
  2. The implicated bytes are main's, verbatim.
    git diff origin/main HEAD -- test/test_push_branch_gate.py src/kiro_crew/security.py
    is empty, and the two files' md5s match git show origin/main:<path> exactly.
  3. They fail from those exact bytes. Running the class in this worktree —
    pytest test/test_push_branch_gate.py::TestUnrecognisedOptionsReadProtectively
    reproduces 6 failed, 30 passed deterministically. These are pure assertion failures
    about shell-token parsing, not environment-shaped ones, which is also why the same six
    land identically on the Linux and the Windows runner.

No follow-up has landed: git log <this head's base>..origin/main -- test/test_push_branch_gate.py src/kiro_crew/security.py is empty across the five commits main has added since. So there is
nothing to rebase onto yet, and nothing here for this PR to change — editing a security test
this PR does not own to make its own lane green would be exactly the wrong move.

Coverage Gate — downstream of that shard, and it says so

Its own decision line, from the job log:

coverage-combine=skipped  frontend-test=success  frontend-coverage-merge=success
backend-test=failure  only_backend=false  only_frontend=false

frontend-test=success and frontend-coverage-merge=success are the part that was mine: the
per-file floor breach on useChatPageTranscriptController.tsx is fixed (77.4% → 83.51%, five
new direct tests, no baseline touched — see the PR body). This lane now fails only because
backend-test=failure, i.e. it is the same single failure as the shard above, not a second one.

CodeQL — unchanged, still main's standing alert

js/insecure-randomness at website/src/pages/ChatPage.tsx:981, which is main's open alert
#639 at :5894 in the same file — the identical sink
const sessionId = addDockTerminal(currentProjectRef.current ?? undefined). The tainted source,
mintId at website/src/hooks/useBottomTerminal.ts:58, is touched zero times by this diff;
shortening ChatPage.tsx moved the reported line and nothing else. It does not block
PR Readiness, which reads the dynamic Analyze run.

Opus 4.8 Review — the lane's own plumbing, and the one thing I cannot resolve myself

This is not a finding. The lane fails before the reviewing pass in its
Capture discovery candidates step:

##[error]Discovery produced no [OPUS-DISCOVERY] marker for 745a3f1d0…, so its output
cannot be trusted as a complete candidate list. Failing closed…

The discovery model ran and finished cleanly every time — the SDK result record reads
"subtype": "success", "is_error": false, "num_turns": 25 against a --max-turns 120
budget, 205 s, so it is not turn exhaustion and not the MAX_CANDIDATE_BYTES cap (that branch
prints a different error). Its final message simply did not end with the marker the capture
step greps for, and the gate correctly refuses to validate an unverifiable candidate list.

I re-ran the lane three times (attempts 1-3 of run 33952207071 — it is a standalone
single-job run, so re-running it cancelled no sibling). Identical failure each time, same
step, same message. The same lane returned "no blocking findings" on the immediately
preceding head 96883a23a, whose diff differs from this one only by the five re-placed main
hunks and the coverage tests — so the lane is not reacting to anything new here, and it is not
a flake a fourth retry is likely to clear either.

Nothing on the PR side can produce that marker, so I am stopping rather than guessing.
This needs a maintainer, and there are two honest routes:

  • a repository writer records the lane's own escape hatch —
    /ai-review override fable 745a3f1d001051af430a8cd9d44552714455014a: Opus discovery emitted no [OPUS-DISCOVERY] marker on three consecutive attempts; the pass completed clean each time and the preceding head reviewed with no findings — or
  • the lane's discovery prompt/capture is looked at as a lane bug in its own right, since a
    12k-line diff reaching this step and losing the trailing marker is a reproducible property of
    this input rather than a property of this PR.

I have deliberately not used the override myself: it is a recorded human judgment with writer
permission behind it, and clearing a fail-closed review gate is not a call an agent should make.

ChatPage.tsx had grown past 8,500 lines and owned every concern the chat
surface has: session and slot lifecycle, the composer and its per-slot
drafts, transcript grouping and scroll, attached resources, every action
callback, and the whole JSX tree. Nothing could be read, tested, or
changed in isolation, and any two edits to unrelated concerns collided.

The page is now a thin host that composes seven purpose-scoped units under
pages/chat/: session, composer, transcript, resources and actions
controllers, plus ChatPageView for the render tree and
ChatPageMessageContent for user-content rendering. Behaviour is
unchanged — this moves code and draws boundaries, it does not redefine
what the page does — so the existing suites keep asserting the same
outcomes against the new seams, and the controllers that gained a
directly reachable surface gained coverage for it.

Behaviours the monolith gained independently of this split are placed in
the module that now owns each, rather than reverted to a pre-split
spelling or left behind in the host:

- The queue-card recipe stays on the shared useQueuedMessageActions hook,
  so the actions controller merges a cancelled card's text into the draft
  instead of assigning over it, and threads the in-flight latch through to
  QueueStack.
- Follow-the-output stays gated on live follow state read through
  vGetFollowRef, so the transcript, session and actions controllers do not
  force-arm an at-bottom flag and cannot yank a reader who scrolled up.
- The opt-in bubble-vanish probe moves with the display-items mirror it
  measures.
- The invisible-only assistant row skip is applied at all five of its
  sites: the render anchor on the page, the renderer, the footer-host scan
  and the turn-item host in the transcript controller, and the
  loose-single host in the view. Regenerate keeps scanning by role,
  because that scan mirrors the history rewrite the server persists and
  must not inherit the renderer's skip.
- The backend's model-withhold verdict stays on the page, beside the rest
  of the model-display derivation.
- The unresumable-resume gate moves to the session controller with the
  swap it guards; its notice renders in the view alongside the other
  pane-level banners, still outside the split / no-slot / transcript
  ternary so a resume arriving with no active slot can be narrated.
- The shared scroll chrome is adopted in the view, where the page's own
  copies of the header fade and jump-to-bottom pill were.
- The composer status stack's re-anchor observer lands in the transcript
  controller that already holds scrollBottom, the follow ref and the
  sibling tip/survey compensation effect its rationale points at, while
  the ref attribute stays on the stack wrapper in the view.
- The composer footer's working-tree badge stays beside the project-git
  query whose `repo` flag gates it and whose cache key it shares, so the
  page keeps that derivation and hands the view three already-reduced
  counts.
- The session MCP report moves with ChatHeaderMenu into the
  message-content module.
- The mobile drawer's history entry splits across two owners because it is
  two concerns: the mint/spend pair, the phase machine and the
  POP-closes-the-drawer effect belong to the drawer in the page, while the
  flag that distinguishes a bookkeeping pop from a Back the user asked for
  is owned by the session controller alongside the other pop refs, since
  that controller's sid effect is what reads it. Registration order holds
  end to end — the sid and URL-sync effects run before the drawer's POP
  effect, so consuming the entry cannot resurrect the outgoing session.
- The session-switch history bookkeeping a POP is checked against — the
  pushed-entry key set and the pending-key claim, plus the shared
  stale-sid repair both POP paths call — lands in the session controller
  with the two effects that write and read it, because a reader that
  guessed from the viewport instead is the defect that bookkeeping
  replaced.
- The transcript's deferral is keyed by slot in the transcript controller
  that owns the display-items mirror, so a session switch renders the
  incoming transcript in its first commit and only same-slot updates are
  deferred.
- The queued-send stash is written where the receipt is read, in the
  actions controller's send, and consumed by the same controller's
  restore, which now merges recovered paths into the staged chips as well
  as the text.
- The user-content render helpers take one options object in the
  message-content module, so a user row's `/chat?sid=…` link switches
  session in place like every other row kind; the transcript controller
  supplies the session triple below the slot-title map it reads, since a
  dependency array is evaluated in the render body.
- The mobile drawer's visual-viewport inset splits the same way the
  drawer itself does: the page reads the viewport and derives the covered
  band, because it already owns the drawer and the view declares no hooks
  of its own, and the two boxes those numbers inset are the view's. Its
  source contract reads the owning module per clause.
- The social-share entry's governance verdict stays beside the rest of the
  dashboard-config derivation on the page and reaches the assistant row
  through the transcript controller that renders it.

The transcript controller inherited enough moved code to fall under the
per-file coverage floor, so the paths that arrived uncovered gain direct
tests through a real page render: a pinned message behind the loaded
window pages history and lands the jump on it, reports unavailable only
once the walk has run out, and is abandoned when the chat it belongs to
is left; and a search hit reports its position, with a re-click on the
selected result travelling back to it rather than advancing.

Source contracts that pin a moved invariant read the owning module per
clause instead of one page file. The moved helpers keep no compatibility
re-export on the page: the website is a bundled SPA, so the only importers
of the old path were repository tests, which now read the owning module
like their neighbours. No extraction leaves a zero-consumer export behind
either — the seams the split needs are exported, and nothing beyond them.

One ordering invariant the split creates is explicit rather than
incidental. The running-state reconcile effect is registered above the
actions controller, because the controller's auto-send effect reads that
reducer's output live: `send` asks `selectComposerBusy(store.getState())`,
which reads `chat.slotRunning`, to decide whether to draw an optimistic
user bubble. Arriving with `?autoSend=1` on a slot the server already has
running is the racing commit, and reading the stale value there draws a
bubble for a message the backend answers with its own queued row, leaving
the turn in the transcript twice. A source-position contract pins the
order and every link that makes it load-bearing, so a later move of either
end fails loudly instead of silently duplicating a turn.

The feature map's Sessions and Worktrees rows name the extracted owners,
since ChatPage.tsx alone is no longer the answer to "where does this
live".

Co-authored-by: Kiro Crew <noreply@kirodotdev.github.io>
@bolichen97

Copy link
Copy Markdown
Collaborator Author

Stand-down: this PR is superseded by an incremental split, not by main

Recorded so the next person does not have to redo the investigation. Nothing was pushed; the branch stays at 2c3df4a3241047c7ea8947a499e2761d31916404.

1. The conflict is architectural, not mechanical

A trial git rebase origin/main (95 commits behind at the time) produced 16 conflict hunks in website/src/pages/ChatPage.tsx, several of them 700–1,300 lines of "main edited inside a region this PR deleted", plus 13 catalog files and one test. The decisive collision is #8689 (2b9e5d916, chat-core RFC P2 transport, third slice after ChatPane #5909 / ChatEmbed #8599 / SideChat #8655), which routes ChatPage's send and steer through website/src/chat-core/transport/sendTurn.ts:

  • main ChatPage.tsx:47 imports sendTurn; call sites at :1696 (steer) and :5385 (send); readSendReceipt hits in main's ChatPage.tsx: 0.
  • This PR's head does not reference sendTurn anywhere (0 hits) and freezes the hand-rolled path main just removed into the new controllers: useChatPageActionsController.ts:71 imports readSendReceipt, :753 new AbortController(), :833 await readSendReceipt(response); useChatPageComposerController.tsx:343 still calls api.steerChat, which feat(chat-core): route ChatPage's send and steer through the transport #8689's message notes has no send-path consumer left on this surface.

Taking both sides leaves two parallel send mechanisms; taking this PR's side reverts #8689 on this surface. Neither is a resolution a worker can justify unilaterally. A second, smaller axis: main's ChatPage.tsx now carries 11 ErrorNotice sites (#8816, #8867) that this PR's ChatPage.tsx (0) and ChatPageView.tsx (3) do not — the same errors-use-error-notice rule GPT 5.6 blocked on earlier.

2. The Opus 4.8 red is a lane completion-variance on an oversized diff, not a code finding

Job log for the 2c3df4a32 run: discovery finished is_error: false / subtype: success after 16 turns, but the execution output carries no result field, so the lane failed closed on "no [OPUS-DISCOVERY] marker". Identical cause on 745a3f1d0 (25 turns). CI's own pre-check warns the PR changes 21,290 reviewable lines (> 15,000). The lane had passed on ~20 consecutive earlier heads (the last success on 96883a23a took 48 turns / 37 min); nothing in claude-review.yml or the prompts changed on main in between. Every rebase makes the diff larger, so the probability of a clean pass only falls. PR Readiness' single blocking item is this lane (CodeQL is counted passed; DISPOSITION_OK: true, zero disposition violations).

3. Not superseded

main's ChatPage.tsx is still a 10,169-line file with no controller split under website/src/pages/chat/; this PR would take it to 2,189 lines. The remaining increment is real and unabsorbed. That is why the recommendation is to re-cut, not to drop.

4. Where the work goes instead

The split continues as slices small enough for one review pass, ordered by distance from the send path, each moving main's current bytes:

  1. #8978ChatPageMessageContent.tsx (header menu, row-key helpers, mintSendId, user-bubble renderers). Open.
  2. useChatPageSessionController.ts — session identity / ?sid sync / tabs / auto-create. Prepared locally, opens next.
  3. useChatPageResourcesController.tsx — panel tabs, sources, file/folder/artifact/diff open, uploads, snips, drops. Prepared locally.
  4. useChatPageTranscriptEarlyController — pre-virtualizer transcript state and the pinned-prompt banner (without the chip state main reverted in revert: minimize the pinned turn banner to a corner chip (#6783) #8714). Prepared locally.
  5. Post-virtualizer transcript hook, composer, actions (rebuilt on sendTurn), and ChatPageView follow once their dependencies land.

The dispositions already recorded here (facade removal, lng: 'en' byte preservation, coverage-file shape, CodeQL #639) carry over as arguments for the slices.

Recommendation: close this PR and let the slices carry the goal. Left for the author to click — this is bolichen97's PR with its review history, and this comment stops driving it.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Split plan — the stack is open

Following up the stand-down note above with the concrete PRs. They are stacked: each targets the previous one's branch, so read each diff relative to its own base. Merge order is 1 → 2 → 3 → 4; after each merge the next PR is retargeted to main and rebased.

# PR Moves out of ChatPage.tsx Base
1 #8978 pages/chat/ChatPageMessageContent.tsx — header menu, row-key helpers, mintSendId, user-bubble renderers main
2 #9072 pages/chat/useChatPageSessionController.ts — session identity, ?sid sync, tabs, slot auto-create #8978
3 #9078 pages/chat/useChatPageResourcesController.tsx — panel tabs, sources, file/folder/artifact/diff open, uploads, snips, drops #9072
4 #9079 pages/chat/useChatPageTranscriptController.tsxuseChatPageTranscriptEarlyController: pre-virtualizer transcript state and the pinned-prompt banner #9078

Every slice moves main's current bytes (not this PR's), so each one already carries the sendTurn, ErrorNotice and uploadHint changes that collided here, and none reintroduces the pinned-prompt chip main reverted in #8714. Each is a single commit, well under the 15k-line review ceiling (the largest is ~900 lines changed).

Still to come, in dependency order once the stack lands: the post-virtualizer transcript hook (needs slice 4 and slice 1), the composer controller (its main region interleaves with the sendTurn steer mutation and the ErrorNotice/uploadHint state, so it is rebuilt rather than moved), the actions controller (rebuilt on sendTurn as the only send exit — none of this PR's hand-rolled POST comes across), and ChatPageView.tsx last.

Recommendation stands: close this PR and let the stack carry the goal — your click, not mine.

@bolichen97

Copy link
Copy Markdown
Collaborator Author

Stack order update: after #8978 merged, the remaining order is now #9079 (pre-virtualizer transcript state) → #9072 (session controller) → #9078 (resources controller). Reason: the per-file coverage floor on ChatPage.tsx — moving the 99%-covered session code out first dropped the page to 79.7% in CI, while moving the pinned-prompt / nav-scroll block out first (the least-covered region, now covered directly in its own module) keeps every intermediate state above 80%. Numbers are on #9072. Content of the three slices is unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge conflict Branch has merge conflicts with its base — author must resolve before merge readiness: action required A blocking check or review needs attention

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dev Fleet restart/sync give no progress feedback, so users fire them twice

4 participants