Repository navigation
refactor(frontend): collapse the duplicated layers a review turned up - #114
Merged
Merged
Conversation
The coverage config's comment claimed the .tsx components stayed out of the denominator; they did not. `include` only decides which unloaded files are added to the report — a file a test imports is measured whether or not it matches, so every component pulled in transitively (providers/settings.tsx, ContextRing.tsx, the two CodeMirror hooks) was counted as uncovered, along with the React hooks no node-environment test can render and the OS/framework boundaries invariant #1 exempts. The number that reached review was 61%, against an 80% bar, for a suite whose actual subject sits at 95%. Exclude what the suite cannot reach so the summary means what invariant #1 says it means. Also drop components/ui/card.tsx: a stock shadcn primitive, never imported since it landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
Five hooks were each rebuilding the same sequence-guarded fetch — a ref counter so a late answer cannot land on a newer request, the loading/error pair, the focus listener — and the copies had drifted: the diff hook skipped focus, the checkouts hook swallowed its error, and only the list blanked its rows when the request behind them changed. useRemoteResource owns the skeleton and makes each of those a named option instead of an omission. Its key is the contract: it stands for the whole request, because `load` is read through a ref so callers can pass an inline closure, and a key missing an argument is a hook that answers with the previous request's data. useDiffEditor and useFileEditor were the same file twice, differing in their extension list and in what a selected doc line means afterwards. The view lifecycle moves to useCodeMirrorView; the two keep their names and their distinct selection contracts as thin wrappers. No behaviour change intended: same requests, same invalidation, same empty and error states. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
…hows Pulls.tsx held six components in 732 lines: the screen, the pull request in full, its merge dialog, the skeleton, the empty state and a notice. The screen and the view have genuinely separate jobs — which pull request is in view and what an action there means for the checkout it lives in, against the header and the four tabs that read one — and the file gave no seam between them. Each component moves to a file named after it. The merge dialog takes its seed messages with it as mergeEditFor, which is where the "what GitHub would have written" decision belongs; the view keeps the in-flight state of the actions it owns and nothing else. Pulls.tsx: 732 -> 255 lines, and no file in the folder is now above 330. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
Closing a worktree's last session is a three-state question — keep the checkout, remove it, or confirm again because it is dirty — and it lived as five functions and two useStates inside SessionSidebar, interleaved with the sidebar's own layout, resize and card wiring. useWorktreeClose owns the state machine; the sidebar keeps only the two dialogs that render it. SessionGroup was forwarding three callbacks it never read, each closing over nothing but a session id, and a projectName that was already baseName of the projectPath beside it. Select, rename and open-a-terminal are wired where the cards are; the sidebar keeps close (its dialog) and reorder (its sibling groups), which are the two that genuinely carry its state. SessionSidebar: 327 -> 253 lines. SessionGroup: 14 props -> 11. The suite cannot render either of these, so this was verified by reading the diff and by tsc; the sidebar still wants a pass in `task dev`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
Eight sites hand-rolled the same localStorage read, each with its own idea of what a bad stored value means: the theme reader accepted anything the array contained, the numeric ones rejected a non-positive number, the context-usage flag treated every string but "false" as on, and the two collapsible panels wrote the same "1 = hidden" encoding through opposite-looking expressions. lib/prefs.ts splits it the way parsePullsSort already did — pure parsing the suite owns, a two-line boundary it does not — and every reader now falls back for a value this build does not understand rather than each deciding for itself. localStorage is no longer touched anywhere else. usePanelVisible is the collapsible navigator (the pull request list, the changed-files tree), keeping the stored encoding so no install loses its choice. One behaviour change, deliberate: a flag holding something writePref never emitted now reads as its default instead of as on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
The three loading skeletons each rebuilt the same ragged-width bar stack, and the footer built its Tooltip-wrapped button five times over two nearly identical class strings — the accent fill marking an open dock panel spelled out once per button. SkeletonLines takes the widths and owns the idiom; the ragged sets stay with their callers, because what is arriving differs (prose, file names, code). The pull request list keeps its paired rows explicit: a title over a subtitle is not a line stack. FooterButton carries the strip's one shape — glyph, tooltip, pressed fill — with `wide` for the segments that put a readout beside the glyph. That also clears the last template-literal classNames outside components/ui, so cn() is now the only class-merging helper in the app, as CLAUDE.md says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
The notifications badge announced its count through an aria-label on a plain span, which has no role and so announced it to nobody; the count moves into the trigger's own accessible name and the dot goes aria-hidden. That is one real screen-reader bug out of the a11y warning backlog, not a lint appeasement — the rest stay warnings. THEMES and TERMINAL_THEMES were documented as driving the Appearance picker, but the picker redeclared both lists, so adding a theme meant editing two places and a mismatch was silent. The options are built off the exported arrays; a missing label is now a type error. AppUpdateGate held the app's only dependency-array-less effect, re-registering the manual checker on every render. It reads the latest checkNow through a ref and registers once. Also: the store setting keys move out of the settings panes into lib/project-settings.ts, since the contract they mirror is the Go store's (internal/store/settings.go) and not the pane's; IconAction's props interface stops shadowing the component; and the one hand-inlined error narrowing goes through errorText. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhBpWr7np1JiiiQhfaa4dV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A maintainability pass over
frontend/, no user-facing change — hence noCHANGELOG.mdentry. Seven commits, each one finding on its own.The one worth reading first
vitest.config.tsclaimed the.tsxcomponents stayed out of the coverage denominator. They did not:includeonly decides which unloaded files join the report, so anything a test imports was measured regardless — components pulled in transitively, the React hooks no node-environment test can render, and the OS/framework boundaries invariant #1 exempts.The number reaching review was 61% against an 80% bar, for a suite whose actual subject sits at 95%.
a074fd7excludes what the suite cannot reach so the summary means what invariant #1 says it means. Same tests, same assertions — only the denominator moved.Duplication removed
ae8ffab— five hooks each rebuilt the same sequence-guarded fetch, and the copies had drifted: the diff hook skipped the focus refetch, the checkouts hook swallowed its error, only the list blanked its rows when the request behind them changed.useRemoteResourceowns the skeleton and each of those becomes a named option instead of an omission.useDiffEditor/useFileEditorwere the same file twice; the view lifecycle moves touseCodeMirrorViewand both stay as thin wrappers with their distinct selection contracts.1592ccc— eight sites hand-rolled the samelocalStorageread, each with its own idea of what a bad stored value means.lib/prefs.tssplits it the wayparsePullsSortalready did: pure parsing the suite owns, a two-line boundary it does not.localStorageis no longer touched anywhere else.usePanelVisiblecovers both collapsible navigators, keeping the stored encoding so no install loses its choice.faa1b88— oneSkeletonLinesbehind the three loading skeletons; oneFooterButtonbehind the status strip's five. That also clears the last template-literalclassNames outsidecomponents/ui, socn()is now genuinely the only class-merging helper.Files that had grown past their job
3e51c8b—Pulls.tsxheld six components in 732 lines. The screen (which pull request is in view, what an action there means for the checkout it lives in) and the view (header plus four tabs) have separate jobs and no seam between them. Each moves to a file named after it: 732 → 255, nothing in the folder above 330.e0f854c— the worktree keep/remove/force question was five functions and twouseStates interleaved with the sidebar's layout and resize wiring; it becomesuseWorktreeCloseand the sidebar keeps only the two dialogs.SessionGroupwas forwarding three callbacks it never read plus aprojectNamealready derivable from theprojectPathbeside it.SessionSidebar327 → 253;SessionGroup14 props → 11.Behaviour changes
Two, both deliberate:
writePrefnever emitted now reads as its default instead of as on.f1511b5fixes a real screen-reader bug: the notifications badge announced its count through anaria-labelon a barespanelement, which carries no role — so nothing announced it. The count moves into the trigger's own accessible name and the dot goesaria-hidden. The review button in the footer also gained a label it never had, viaFooterButton. Biome a11y warnings 13 → 12; the rest of that documented backlog stays.Same commit also makes
THEMES/TERMINAL_THEMESactually drive the Appearance picker (it redeclared both lists, so a mismatch was silent — a missing label is now a type error), givesAppUpdateGatethe dependency array it was missing, and moves the store setting keys tolib/project-settings.ts, since the contract they mirror isinternal/store/settings.go's and not a settings pane's.Verification
Local gate green:
gofmt/go vet/go test,biome check,tsc,vite build, 491 frontend tests.The suite cannot render components, so the render path was driven for real — the binary built and served to a headless Chromium:
aria-pressed;html.darkand the stored pref;No page errors. What this did not reach: the happy path of
PullRequestViewandPullsEmptyStateboth needghand a live PR, so they are worth a pass intask devbefore merging.