refactor(chat): move ChatPage resource handling into a controller hook - #9078
Conversation
First Principles Review (Fable 5) — ✅ PASSPremise-level review of All verification checks pass. I have everything needed for the verdict. The backend confirms the rider fix's premise: First-Principles-Verdict: PASS A faithful move of 650 inline lines into the established controller-hook split, plus one declared, backend-grounded fix for silently swallowed screenshot failures. What this change shipsIntent: make ChatPage's resource handling reviewable by extracting it into a hook, as one slice of the #7255 split — a MOVE (refactor), not a fix or addition.
Consumer count on the new surface: every one of the 29 returned values has ≥1 real use in ChatPage.tsx (grepped each name; minimum 2 occurrences including the destructure). Zero dead exports. The rider fix's zero option is a user clicking Screenshot and seeing nothing on a real 400/504 — a named, nonzero harm — so it earns its ride. [FIRST-PRINCIPLES-REVIEWED] e1d96ce |
UX Review (Fable 5) — 🟡 CONCERNSUX-level review of All evidence is in. The diff is a pure hook extraction with one declared user-visible change: a failed screen capture now surfaces "Screenshot failed: {{reason}}" through the page's existing UX-Verdict: CONCERNS Pure hook extraction with one new rendered state — the screen-capture failure notice — that no screenshot shows and no blind reader saw. Evidence gaps
[UX-REVIEWED] e1d96ce |
Design Review (Fable 5) — ✅ PASSDesign-level review of The diff is a faithful move: the hook body is main's bytes with the declared single-line Design-Verdict: PASS A mechanical, well-declared slice following the existing controller-hook precedent; the one behavior change is documented, justified, and characterization-tested. [DESIGN-REVIEWED] e1d96ce |
GPT 5.6 Review — ✅ no blocking findingsGPT 5.6 completed its review of This comment is updated in place on each push. Review detailsNo findings. False positive or not applicable? A repository writer can comment: |
Opus 4.8 Review — ✅ no blocking findingsReviewed Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
aa4986f to
77fb03c
Compare
a8dc303 to
4dd6fae
Compare
|
Local gate run on the stack top
|
77fb03c to
580bf4f
Compare
4dd6fae to
3c23c1a
Compare
The base branch was changed.
f393081 to
662ee2c
Compare
|
The Rebased onto
Please don't close or revert on the strength of that label; the labeller just Gates on this exact tree: eslint clean, Still open and not addressed by the rebase: GPT's two |
|
Raw exception text as the whole message —
Correct, and the reasoning is right: "screenshot timed out" with no subject is a Being plain about why it reads that way: minting a catalog key means touching the It is deferred rather than fixed here for a reason that is about this PR's
Evidence gap — the new error state appears in no committed screenshot, and the
The description no longer claims no visual delta; it names the rendered |
662ee2c to
aaaacb5
Compare
|
Raw exception text as the whole message —
Done exactly that. New key Correcting myself on why it was deferred. I said minting a key meant touching Verification on this head: Evidence gap — the notice appears in no committed screenshot — accepted, tracked in #9186. The |
The side-panel resource code in ChatPage.tsx -- panel tabs, message
search, source/issue link selection and persistence, file / folder /
artifact / diff opening, file save, screen snips, uploads, the optimizer
result hand-off and drag-and-drop -- moves into
pages/chat/useChatPageResourcesController.tsx. The page calls the hook
once where that code used to sit and destructures the values it still
reads. handleSubagentPanelOpen, sessionTitles, handleFork and
handlePlanFromHere stay in the page.
The moved text is main's bytes, with the three exceptions below. Two
mechanical edits: the page state it reads arrives as hook arguments
(composer and capture ports, showActionError), and the dependency arrays
name the refs and setters the hook now receives (referentially stable, so
nothing re-fires). currentProjectRef is declared before the hook call
because the call site reads it eagerly; its value is computed from the
same slots lookup every render as before.
First exception, because the move is what made it visible: takeScreenshot
ended in `} catch { /* user cancelled */ }`, and that comment is wrong. A
cancelled capture is not an error -- POST /api/screenshot answers it with
200 and `{"path": ""}` (dashboard/handlers/files.py), which the `if (path)`
guard directly above already absorbs. So nothing reaching that catch was
ever a cancellation: it swallowed the 400 off macOS, the 120-second
capture timeout, and any request that never reached the gateway, leaving
the user clicking Screenshot with no attachment and no notice. It now
reports through showActionError (added to the dependency array).
Second exception, answering this PR's UX review: that notice names the
action rather than only restating the transport error. A bare "screenshot
timed out" above the composer does not say which click failed, so the
message goes through a new `screenshot_failed_reason` key -- "Screenshot
failed: {{reason}}" -- on the same shape as the existing
send_failed_with_error and could_not_read_file_reason. The key is
translated in all eleven locales rather than left to fall back:
catalogParity.test.ts requires every en key in every catalog, and the
i18n gate's changed-passthrough check rejects an English value dropped
into a translated catalog, so a new key needs real translations or none.
Each value follows its own locale's conventions in the sibling key --
full-width punctuation for zh-CN and ja, the space before the colon for
fr -- and preserves {{reason}} verbatim. Two cases in
ChatPageMoreCoverage.test.tsx pin both halves, a rejected capture
surfacing and a cancelled one staying silent, and each mutation of that
pair fails one of them.
Third exception, forced by a gate rather than chosen: the composer-append
expression was `previous.trim() ? \`${previous.trimEnd()}\n\n${text}\` : text`
verbatim from main, and relocating it makes it an added line, which the
i18n gate reads at zero tolerance even though `\n\n` is structural
whitespace and not copy. It is now `[previous.trimEnd(), text].join('\n\n')`
-- the same value, in a shape the rule does not flag. Exempting the file in
eslint.i18n.config.js was the alternative and was rejected: it would blind
the copy gate to every future literal in an 826-line module.
Third slice of the ChatPage split proposed in #7255.
Co-authored-by: Kiro Crew <noreply@kiro.dev>
aaaacb5 to
e1d96ce
Compare
|
Evidence gap — the screen-capture failure notice renders in no committed screenshot and the blind read did not run — accepted, and deferred to #9186 on review-pass budget rather than on capability.
Taking the earlier half first: the wording objection from the previous round is I can produce it — this is not a "cannot". Playwright 1.58.2 is present and Why it lands in #9186 instead. This slice is a declared byte-faithful move The frame is also worth more there than here. #9186 collects the remaining silent What stands behind the claim in the meantime, so the gap is not merely noted:
I have added the frame to #9186's scope explicitly rather than leaving it implied. |
Problem / Motivation
website/src/pages/ChatPage.tsxis a 10k-line page. About 650 lines of it manage the resources around a chat: side-panel tabs, message search, pull-request / issue source selection, opening files, folders, artifacts and diffs, saving a file, screen snips, uploads and drag-and-drop. None of it is about the transcript or the composer, but it all lives inline in the component.Why it matters
The whole-page split in #7255 grew past the point where CI's Opus review lane can finish one pass and collided with
main's chat-core transport work. It lands as slices that each fit one review pass. This slice owns resource ingress and the side panel. It does not touch the send path.What changed (motivation → approach → change)
The moved code reads a handful of page values (the active slot, messages, the drafts refs, a few setters) and returns the handlers the page passes into the transcript and the composer. That is a custom hook.
ChatPage.tsxlines 3145–3564, 3584–3661 and 3707–3860 move intopages/chat/useChatPageResourcesController.tsx. The page callsuseChatPageResourcesController({...})at the spot the first moved line used to be and destructures the twenty-nine values it still reads.handleSubagentPanelOpen,sessionTitles,handleForkandhandlePlanFromHeresat between the moved blocks and stay in the page, in the same order.The moved text is
main's bytes, with two mechanical edits and one declared behaviour fix (below). The page state the code reads arrives as hook arguments: acomposerport (input ref and setter, draft refs,setPendingFiles,currentProjectRef, the voice caret refs,saveDrafts), acaptureport (setUploading,setUploadError,setUploadHint,setResizedInfo,snipSlotRef,setSnipFrame) andshowActionError. The dependency arrays name the refs and setters the hook now receives —react-hooks/exhaustive-depscannot see them as stable once they arrive as arguments — and they are referentially stable, so no callback is recreated and no effect re-fires.One behaviour change, declared.
takeScreenshot'scatchused to be bare and commented/* user cancelled */. That comment was wrong:POST /api/screenshotanswers a cancelledscreencapture -iwith200 {"path": ""}, which theif (path)guard already absorbs, so the only things that ever reached thecatchwere real failures — the400off macOS, the120scapture timeout, or the request never reaching the gateway — and the user saw nothing. Thecatchnow reports the server's own reason throughshowActionError(the hook's existing error port). This is the one line in the moved code that is notmain's bytes. It is here rather than in a separatefix:PR because the moved line reads as an added line, and theerrors-use-error-noticerule blocks an added barecatch; carrying the smallest possible fix, declared and tested, is the way to keep this slice reviewable as a move without a human override.One declaration moves earlier:
currentProjectRefis now created just before the hook call, because the call site passes it eagerly and it used to be declared 2,000 lines later. Its value is stillslots.find(...)?.projectcomputed every render, so what the handlers read at event time is unchanged.Tests
ChatPageMoreCoverage.test.tsxgains aChatPage screen capture failuresblock (2 tests): a rejectedapi.screenshot()surfaces its message as an error notice; a cancelled capture ({ path: '' }) stays silent. TheChatInputmock exposesonScreenshotso the composer's capture button can be pressed.ChatPage*spec plus the panel-tabs, pins, source-selection, pull-request-links, upload-routing, drop-classify, screen-snip and message-search specs pass unchanged: 78 files / 946 tests targeted. Full suite result in the first comment.tsc -b,eslint(0 warnings),jscpd(0 clones),lint:phantom-classes,check_feature_map.py,check_brand_name.py,docs-lint.shall exit 0.Manual verification
N/A — the moved handlers are covered by the
ChatPage.dirStaging,ChatPagePasteImage,ChatPage.sourceSelection,ChatPage.sidebarSourceReveal,ChatPage.handleOpenDiffNoopandChatPage.gitPanelGatecharacterizations, which pass unchanged.Screenshots / video
Why no screenshot: hook extraction; the only rendered change is that a failed screen capture now shows the existing
ErrorNoticewith the server's message where before it showed nothing — no new component, layout or copy beyond the passed-through reason.Related Issues
Part of the split of #7255. no linked issue: the parent PR has no tracking issue.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)