feat(ui): tab cycles files/diff focus with sidebar+divider color shift#274
feat(ui): tab cycles files/diff focus with sidebar+divider color shift#274aldevv wants to merge 3 commits intomodem-dev:mainfrom
Conversation
Add a `"diff"` focus area so Tab cycles between the sidebar and the diff stream rather than between the sidebar and the filter input. Default focus on launch is `"diff"`. The sidebar's top border and the pane divider shift to `theme.accent` while the sidebar holds focus and revert to `theme.border` once focus returns to the diff. The filter input is reached with `/`; Tab inside that input stays in the input so users can type the literal character. When the sidebar holds focus, `↑`/`↓` step the file selection (next or previous visible file) instead of scrolling the diff. The diff still follows because `selectFile` aligns the file header to the top. Adds `moveToFile(delta)` to the review controller. The step logic lives in a shared `stepFileInList` helper that both `moveToFile` and `findNextAnnotatedFile` delegate to, so the no-current-selection edge case (first ↓ landing on index 0, not 1) is fixed in one place. The `FocusArea` type lives in `src/ui/lib/focus.ts` so the App and the keyboard hook share one source of truth.
Greptile SummaryThis PR reworks the keyboard focus model to add a
Confidence Score: 5/5Safe to merge — the changes are confined to keyboard routing and visual feedback, with no data mutations or async side-effects. All three focus-area transitions are covered by integration tests, stepFileInList has eight dedicated unit tests including every edge case called out in the PR description, and the findNextAnnotatedFile refactor is a clean delegation that also fixes the skip-index-0 bug. No logic changes touch persistence, networking, or auth. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
Launch([App Launch]) --> FD[focusArea = 'diff']
FD -->|Tab| FF[focusArea = 'files']
FF -->|Tab| FD
FD -->|'/'| FI[focusArea = 'filter']
FF -->|'/'| FI
FI -->|Esc via StatusBar| FD
FF -->|up/down| MF[moveToFile stepFileInList clamp]
FD -->|up/down| SD[scrollDiff step]
FI -->|any key| OWN[Filter input owns keystroke]
MF --> SF[selectFile alignFileHeaderTop]
SF --> VF[diff scrolls to new file header]
FF -->|border| ACC[SidebarPane + PaneDivider borderColor = theme.accent]
FD -->|border| NRM[SidebarPane + PaneDivider borderColor = theme.border]
style FD fill:#334,stroke:#66f
style FF fill:#343,stroke:#6f6
style FI fill:#433,stroke:#f66
Reviews (1): Last reviewed commit: "feat(ui): tab cycles files/diff focus wi..." | Re-trigger Greptile |
Why
Changesets often have many files when you really want to read one in particular. Tab cycled the sidebar and the filter input instead of the diff, and nothing visually marked which pane held focus, so picking a file and settling into it was harder than it should be.
User-visible
Tabpreviously toggled between the sidebar file list and the file filter input, which made the filter the only path back to keyboard control of the diff. After this changeTabcycles between the sidebar and the diff stream itself, default focus on launch is the diff, and the filter is reached with/. While the sidebar holds focus its top border and the pane divider shift totheme.accent, and↑/↓step the file selection instead of scrolling the diff (the diff still follows because the new file header is aligned to the top).What it does
"diff"FocusArea, hoisted intosrc/ui/lib/focus.tssoApp.tsxanduseAppKeyboardShortcuts.tsshare one source of truth.focusAreais"diff"on launch.SidebarPaneaccepts afocusedprop;PaneDivideracceptshighlighted. Both shift their border totheme.accentwhen the sidebar holds focus.moveToFile(delta)on the review controller, wired through the keyboard hook so↑/↓step files when the sidebar is focused.stepFileInList(files, currentId, delta, "clamp" | "wrap")helper. BothmoveToFile(clamp) andfindNextAnnotatedFile(wrap) delegate to it, which fixes a no-current-selection edge case: with no file selected, the first↓now lands on index 0 instead of skipping to index 1.Tests
bun test src/ui/lib/files.test.ts— 8 new unit tests forstepFileInList(empty list, zero delta, clamp/wrap, boundary clamping, no-selection edge cases, single-item wrap).bun test src/ui/hooks/useReviewController.test.tsx—moveToFilecontroller test stepping forward, backward, and clamping at the last file.bun test src/ui/components/ui-components.test.tsx—SidebarPanetop-border colour shifts totheme.accentwhenfocused.bun test src/ui/AppHost.interactions.test.tsx—Tabcycles sidebar ↔ diff focus and↓only steps the file selection while the sidebar holds focus (also covers default-focus ="diff").test/pty/ui-integration.test.tsupdated to match the new "Toggle files/diff focus" menu label.bun run typecheckandbun run lintclean.No docs or workflow changes needed.