refactor(web): decompose RunChat.tsx + RunChat.test.tsx below line caps #196

Merged
dominik.polakovics merged 2 commits from afk/194 into main 2026-07-22 01:17:41 +02:00

Closes #194

Decomposes the repo's two worst churn×size files along the seams agreed in the issue. Behavior-preserving: no visual or behavioral change, tests moved verbatim between files with assertions untouched.

Component split (commit 1)

RunChat.tsx (2956 → 691 lines) keeps the route entry, data wiring, scroll controller, panel state and JSX composition; six named siblings land under routes/runchat/:

File Lines Responsibility
runchat/ChatHeader.tsx 580 header row + overflow menu
runchat/Dialogs.tsx 570 question/dialog surface (incl. submitOnEnter — its only callers are dialog code, adjusted per the issue's reality clause)
runchat/messageFeed.ts 413 the ADR-0016/0047 refetch protocol (accumulated window, FULL/LIGHT modes, token guard, debounce, rotation, SSE wiring) as a createMessageFeed primitive — extracted because the sibling moves alone left the route at ~1010 lines, over the cap
runchat/Composer.tsx 402 input, send, interrupt
runchat/Messages.tsx 225 transcript renderers
runchat/Markdown.tsx 222 the issue #13 banner-delimited renderer
runchat/shared.ts 7 capitalize (route + composer)

Moves are verbatim; the only rewrites are the injected messageFeed seam (run id, events, run refetch, scroll callbacks). Proof: the then-untouched monolith test file passed 145/145 against the split source.

Test split (commit 2)

RunChat.test.tsx (4325 → 673 lines, route wiring) plus runchat/ suites: Dialogs.test 1346, Messages.test 814, Composer.test 608, ChatHeader.test 425, and harness.tsx 480 (fixtures, API/SSE/matchMedia stubs, mount helper, hook installer; source-class per #193). Mutable server-state lets became fields on the harness's exported h object (ESM importers cannot reassign imported bindings) — a mechanical word-boundary rename, verified: test titles and expect() counts identical to the monolith (145 / 601).

providerNeutral.test.ts gains a !./**/harness.tsx glob exclusion: the harness carries the provider fixtures the monolith always had, but isn't .test.ts*-suffixed (naming it so would make vitest collect it as an empty suite).

Gates

npm run build (tsc + vite), eslint ., prettier --check, vitest run → 50 files / 871 passed, all green. Every resulting file is under its #193 cap (source ≤ 691/1000, tests ≤ 1346/2000).

#193 sequencing

#193 (ratchet gate) has not landed — there is no baseline.json on main yet, so this PR has no entries to drop. Whichever lands second reconciles trivially: if #193 lands after this, its seeded baseline simply won't include the two RunChat files (they're under cap now); if this PR rebases over a landed #193, delete the two entries / run the baseline regeneration command in the merge.

🤖 Generated with Claude Code

Closes #194 Decomposes the repo's two worst churn×size files along the seams agreed in the issue. **Behavior-preserving**: no visual or behavioral change, tests moved verbatim between files with assertions untouched. ## Component split (commit 1) `RunChat.tsx` (2956 → 691 lines) keeps the route entry, data wiring, scroll controller, panel state and JSX composition; six named siblings land under `routes/runchat/`: | File | Lines | Responsibility | |---|---|---| | `runchat/ChatHeader.tsx` | 580 | header row + overflow menu | | `runchat/Dialogs.tsx` | 570 | question/dialog surface (incl. `submitOnEnter` — its only callers are dialog code, adjusted per the issue's reality clause) | | `runchat/messageFeed.ts` | 413 | the ADR-0016/0047 refetch protocol (accumulated window, FULL/LIGHT modes, token guard, debounce, rotation, SSE wiring) as a `createMessageFeed` primitive — extracted because the sibling moves alone left the route at ~1010 lines, over the cap | | `runchat/Composer.tsx` | 402 | input, send, interrupt | | `runchat/Messages.tsx` | 225 | transcript renderers | | `runchat/Markdown.tsx` | 222 | the issue #13 banner-delimited renderer | | `runchat/shared.ts` | 7 | `capitalize` (route + composer) | Moves are verbatim; the only rewrites are the injected messageFeed seam (run id, events, run refetch, scroll callbacks). Proof: the then-untouched monolith test file passed 145/145 against the split source. ## Test split (commit 2) `RunChat.test.tsx` (4325 → 673 lines, route wiring) plus `runchat/` suites: Dialogs.test 1346, Messages.test 814, Composer.test 608, ChatHeader.test 425, and `harness.tsx` 480 (fixtures, API/SSE/matchMedia stubs, mount helper, hook installer; source-class per #193). Mutable server-state `let`s became fields on the harness's exported `h` object (ESM importers cannot reassign imported bindings) — a mechanical word-boundary rename, verified: test titles and `expect()` counts identical to the monolith (145 / 601). `providerNeutral.test.ts` gains a `!./**/harness.tsx` glob exclusion: the harness carries the provider fixtures the monolith always had, but isn't `.test.ts*`-suffixed (naming it so would make vitest collect it as an empty suite). ## Gates `npm run build` (tsc + vite), `eslint .`, `prettier --check`, `vitest run` → 50 files / **871 passed**, all green. Every resulting file is under its #193 cap (source ≤ 691/1000, tests ≤ 1346/2000). ## #193 sequencing #193 (ratchet gate) has **not landed** — there is no `baseline.json` on main yet, so this PR has no entries to drop. Whichever lands second reconciles trivially: if #193 lands after this, its seeded baseline simply won't include the two RunChat files (they're under cap now); if this PR rebases over a landed #193, delete the two entries / run the baseline regeneration command in the merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
RunChat.tsx (2956 lines) becomes the route entry (691 lines) plus six
named siblings under routes/runchat/: ChatHeader (header + overflow
menu), Messages (transcript renderers), Markdown (the issue #13
renderer), Composer (input/send/interrupt), Dialogs (question/dialog
surface), messageFeed (the ADR-0016/0047 refetch protocol behind a
createMessageFeed primitive) and a capitalize shared helper. Moves are
verbatim; the only rewrites are the injected messageFeed seam
(runId/events/refetchRun/currentRepoID + scroll callbacks). Behavior
preserved: the untouched RunChat.test.tsx suite passes 145/145.

Closes nothing yet — test split follows in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
refactor(web): split RunChat.test.tsx into per-area suites (#194)
All checks were successful
ci / native (pull_request) Successful in 7m15s
3b0fc444b8
RunChat.test.tsx (4325 lines, 145 tests) becomes the route-wiring suite
(673 lines) plus four area suites under routes/runchat/ — ChatHeader,
Messages, Composer, Dialogs — and a shared harness.tsx (fixtures, API/
SSE/matchMedia stubs, mount helper, hook installer). Test bodies moved
verbatim; the only rewrite is mechanical: the mutable server-state lets
became fields on the harness's exported h object, since ESM importers
cannot reassign imported bindings. Titles and expect() counts are
identical to the monolith (145 / 601), full suite 871/871.

providerNeutral.test.ts excludes **/harness.tsx: the harness carries
the same provider fixtures the monolith test always had, but is not
suffixed .test.ts* (naming it so would make vitest collect it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

[autoland] verdict: pass
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!196
No description provided.