Skip to content

refactor(app): move the sidebar and drawer layout state into a hook - #1766

Merged
devswha merged 1 commit into
mainfrom
refactor/app-sidebar-layout-hook
Oct 23, 2026
Merged

devswha merged 1 commit into
mainfrom
refactor/app-sidebar-layout-hook

Conversation

@devswha

@devswha devswha commented Oct 23, 2026

Copy link
Copy Markdown
Owner

Wave 4 of the App.tsx split. The plan is .omo/plans/app-stores-split.md in the maintainer's workspace. It follows PR #1762.

This wave moves the drawer, sidebar and file-panel layout state out of src/App.tsx into src/lib/useSidebarLayout.ts. The hook runs inside App's render, so updates and effect order are unchanged.

What changes

Three blocks move verbatim, in their order, into useSidebarLayout({ initialLink }):

  1. Drawer: the drawer state (a link can open it), drawerOpenRef, wideScreen, and the effect that closes the drawer on a wide window.
  2. Gestures: the edge-swipe effect and the Escape capture listener that puts the drawer away.
  3. Widths: sidebarCollapsed, sidebarWidth, filePanelAvailable and filePanelWidth.

The hook returns those values, their setters, and the same drawerOpenRef object.

What stays in App.tsx:

  • One call at the first block's position.
  • selectionRef and its layout effect, which sat between the blocks but belong to selection. They now follow the call directly.
  • Every call site.

Listener order: the lastKeyAt keydown listener still comes after the call, so the Escape capture listener still registers first.

Imports: storedSidebarWidth, FILE_PANEL_DOCK_QUERY, storedFilePanelWidth and watchDrawerSwipe leave App's imports.

Hook ceiling: APP_HOOK_CEILING drops from 171 to 163, after moving four useState, one useRef and three useEffect calls.

Checks

  • --color-moved: git diff --color-moved=plain --color-moved-ws=allow-indentation-change origin/main...HEAD shows 56 lines as moved. The other 22 lines are:
    • App's removed imports and the new import;
    • the call line;
    • the ceiling value;
    • the new file's imports, params type, signature, doc comment and return.
  • Moved blocks are exact: each block (lines 365-371, 379-393 and 394-399 of App.tsx on e44fc9f8f) appears byte for byte in the hook.
  • Typecheck: bun run typecheck passes.
  • Unit tests: HERDR_TEST_MODE=unit bun test src/app-hooks.test.ts src/lib/edgeSwipe.test.ts src/lib/sidebarWidth.test.ts src/lib/filePanelWidth.test.ts src/conventions.test.ts: 45 pass, 0 fail.
  • Ceiling test: adding const zz = useState(false); to App.tsx makes it fail.
  • bun run check fast: passes after the rebase (unit 6253 pass, 0 fail, 13 skip in 443 files, 8 at a time, 57.6s)
  • bun run check integration browser: the browser lane exits 0 (every browser script passed, 2036 s), run on this commit before its rebase onto the latest main (which changed only PaneTerminal and server files). The integration lane had two failures, none in code this PR touches: server/api.contract.test.ts "creates, edits, searches, inspects, and closes an owned workspace" (line 1540, fails the same way on unchanged origin/main on this PC) and server/gjc.contract.test.ts "binds same-cwd panes independently by native PID…" (pane.process_info did not yet list the isolated GJC process while other lanes ran on the same PC); rerun alone on this branch, server/gjc.contract.test.ts passes (10 pass, 0 fail). CI runs the integration lane in its own environment.

Outside PRs that touch the same files

No other maintainer PR touches these files. One outside contributor's PR touches src/App.tsx: #1427. It already conflicts with main in App.tsx, and needs a rebase either way.

Move-only: moved lines plus imports, the hook signature and return, one call and the lowered ceiling. No changelog fragment: users see no change.

The drawer, wide-screen close, edge swipe, Escape capture, sidebar
collapse/width and file panel width move verbatim into
src/lib/useSidebarLayout.ts, called where the drawer state stood.
selectionRef stays in App; the hook ceiling is lowered to 163.
Plan .omo/plans/app-stores-split.md wave 4.
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 1b838cb and d8db2ba.

📒 Files selected for processing: 3
  • src/App.tsx
  • src/app-hooks.test.ts
  • src/lib/useSidebarLayout.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Layout
    • Drawer behavior now adapts to screen width when opening a link that requests a visible sidebar.
    • On smaller screens, the drawer can be dismissed with Escape or a swipe gesture. Escape does not close it during text composition or while a dialog is open.
    • Sidebar collapse state and sidebar and file-panel widths continue to reflect saved settings.

useSidebarLayout now manages drawer, sidebar, and file-panel layout state. App uses the hook instead of defining that behavior locally. The hook-call ceiling test threshold is lower.

Changes: Sidebar layout

Layer / File(s) Summary
Layout state and drawer behavior
src/lib/useSidebarLayout.ts
Adds SidebarLayoutParams and useSidebarLayout. The hook initializes drawer state from the initial link and viewport, handles swipe gestures and Escape events, and initializes sidebar and file-panel state.
App integration and hook threshold
src/App.tsx, src/app-hooks.test.ts
App calls the hook for layout state and removes the corresponding local setup. Selection tracking remains unchanged. The hook-call ceiling decreases from 171 to 163.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to d8db2

No actionable behavior change is established, so the PR is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: moving sidebar and drawer layout state into a hook.
Description check Passed The description explains the change, preserved behavior, implementation details, listener order, scope, validation results, and known unrelated integration failures. It uses "## What changes" and "## …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the change, preserved behavior, implementation details, listener order, scope, validation results, and known unrelated integration failures. It uses "## What changes" and "## Checks" instead of the template headings, but it provides the required information.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devswha

devswha commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

Merge pass: move-only PR (wave 4 of the App.tsx split). It merges on its checks without waiting for a CodeRabbit review, as AGENTS.md allows.

Diff. Compared as a multiset of lines, the removed and added lines differ only in:

  • imports;
  • the call line;
  • the ceiling;
  • the new file's wrapper.

Gate review, by a reviewer on a different model than the author's:

  • The move is verbatim. All three blocks moved unchanged and in their original order.
  • Effect order. On main, selectionRef's effect sat between the moved blocks. It is a useLayoutEffect, while the moved effects and the useMediaQuery subscriptions are passive, and none of them touches selection state. So moving them above it changes no order anything can observe, at mount, update or cleanup.
  • Listener order. The Escape capture listener still registers before lastKeyAt. Its stopPropagation() does not suppress the later listener on the same window.
  • Identities. drawerOpenRef, the setters and the lazy initial state keep their identities.
  • Hook count. Counted on the TypeScript AST: 171 → 163.

Tests. 45 unit tests and the typecheck pass at this head (merge-pass run).

All required checks are green and no review thread is open.

@devswha
devswha merged commit 53b8512 into main Oct 23, 2026
9 checks passed
@devswha
devswha deleted the refactor/app-sidebar-layout-hook branch October 23, 2026 14:18
devswha added a commit that referenced this pull request Oct 23, 2026
)

Wave 5 of the App.tsx split. The plan is
`.omo/plans/app-stores-split.md` in the maintainer's workspace, and
plan-reviewer approved it. It follows PR #1766.

This wave moves this device's alert and notification state out of
`src/App.tsx` into `src/lib/useAlerts.ts`. It also moves what the event
stream does for alerts when a pane's status changes or a pane exits.

**This is not purely moved code.** The event-stream effect stays in App,
and three pieces of its handler become calls to hook callbacks, so this
PR waits for a review. Every line the pieces held moves byte for byte,
apart from indentation.

## What moves

Six blocks move verbatim, in source order, into `useAlerts`:
- **(A)** the alert preferences: `alerts`, `alertsRef`, `alertsOn` and
`alertsOnRef`.
- **(B)** `notifications`, `pushOn` and `pushOnRef`.
- **(C)** the status refs: `statusRef`, `turnStartRef`, `lastTurnRef`,
`alertInAppRef` and `alertSoundRef`. `followFocusRef` stays in App.
- **(D)** `dropIn`, the alert-sound player effect, `chime`, and the
sound-unlock effect.
- **(E)** `enableNotifications`, `disableNotifications`, and the push
re-registration effect.
- **(F)** the bell, `bellVisible` and `alertsOffDot`.

In the event-stream effect, three pieces become calls at exactly the
points the lines stood:
- **Roster:** `onRoster(payload.machines)` is the roster's
`seedStatuses` line. The only token that changes is `payload.machines`,
which becomes the parameter `machines`.
- **Pane status:** `onPaneStatus(machine, message,
takeNotificationTargetRef)` covers the status bookkeeping, the in-app
drop and chime, and the system notification. The
`setMachines(applyPaneStatus…)` update stays in App, right after the
call.
- **Pane exit:** `onPaneExited(machine, message,
takeNotificationTargetRef)` covers the ended-turn drop and chime, then
the ended notification. The `herdr-usage-changed` dispatch stays in App,
before the call.

`takeNotificationTargetRef` is declared later in App, so the effect
passes the ref object at event time. The moved lines still read
`takeNotificationTargetRef.current` at the same moment as before.

## Why behaviour is unchanged

**Hook call position:** the hook is called where `notifications` was
declared, before App's `followFocusRef`. The alert values were not read
anywhere between their old declaration and that point. Everything the
hook takes is declared before it: `settings`, `updateSettings`, `t`,
`locked`, `selectionRef`, `drawerOpenRef`, `splitRef`, `splitShownRef`
and `machinesRef`.

**No resubscription:** `dropIn`, `chime`, `onRoster`, `onPaneStatus` and
`onPaneExited` are all `useCallback(..., [])`. The event-stream effect's
dependency list `[locked, scheduleRefetch, dropIn, chime,
reorderCoordinator]` is unchanged, so the stream does not resubscribe.
`enableNotifications` and `disableNotifications` keep
`[updateSettings]`.

**Effect order:** three effects now run earlier in a commit than before:
- **The alert-sound player effect and the sound-unlock effect** now run
before App's load-and-health effect. They only create a player and
listen for taps, independent of loading.
- **The push re-registration effect** now runs before the
load-and-health effect and the event-stream effect.
  - On first load it returns at once, because `locked` is still `null`.
- When the lock opens, it and the event-stream effect re-run in the same
commit, in the opposite order. Both only start asynchronous network
calls (a push subscription; an EventSource) and share no state.

**The bell:** it is computed during render. It reads only values the
hook owns, plus `settings` and `t`.

**Hook ceiling:** `APP_HOOK_CEILING` drops from 163 to 144. That is the
19 hook calls that moved: 3 in A, 3 in B, 5 in C, 5 in D and 3 in E.

## Checks

- **`--color-moved`:** `git diff --color-moved=plain
--color-moved-ws=allow-indentation-change origin/main...HEAD` shows 361
lines as moved. The other 57 lines are:
  - App's narrowed imports, the new import and the call;
  - the three event-stream calls and the `onRoster` line;
- the new file's imports, params interface, message types, signature,
the three callback headers and closers, the doc comment and the return;
  - the ceiling.
- **Moved lines are exact:** blocks A-F appear byte for byte in the hook
(lines 225-232, 496-500, 509-518, 604-647, 744-787 and 1317-1343 of
`App.tsx` on `53b85127e`). The event-stream lines 677-689, 704-709 and
711-714 appear apart from indentation.
- **Typecheck:** `bun run typecheck` passes.
- **Unit tests:** **129 pass, 0 fail across 9 files**, from:

`HERDR_TEST_MODE=unit bun test src/app-hooks.test.ts
src/conventions.test.ts src/lib/notifications.test.ts
src/lib/droplet.test.ts src/lib/alertTurns.test.ts
src/lib/alertSound.test.ts src/lib/push.test.ts
src/lib/notificationTarget.test.ts src/lib/i18n.test.ts`
- **Ceiling test:** adding `const zz = useState(false);` to App.tsx
makes `src/app-hooks.test.ts` fail.
- `bun run check fast`: passes after the rebase (unit 6253 pass, 0 fail,
13 skip in 443 files, 8 at a time, 57.5s)
- **`bun run check integration browser`:** the browser lane exits 0
(every browser script passed, 1816 s), including the alert scripts: the
Alerts item off/on, blocked-permission switch, Settings push test,
notification startup, a tapped pane alert opening
chat/terminal/workspace, in-app alerts (open pane spared, drop-in,
flick, close, timeout, off stays off, split half spared) and the alert
sound (no sound before a tap, Settings toggle with preview, two tabs
chime once). It ran on this commit before its rebase onto the latest
main, which changed only PaneTerminal files. The integration lane's only
failure is `server/api.contract.test.ts` "creates, edits, searches,
inspects, and closes an owned workspace" (line 1540), which fails the
same way on unchanged `origin/main` on this PC; CI runs that lane in its
own environment.

## Outside PRs that touch the same files

No other maintainer PR touches these files. One outside contributor's PR
touches `src/App.tsx`: #1427. It already conflicts with main there, and
in eight other files, so it needs a rebase either way.

No changelog fragment: users see no change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant