Skip to content

fix(development): proper machine-list states, test the mobile sheet close - #2049

Merged
2witstudios merged 4 commits into
masterfrom
pu/development-surface-polish
Jul 13, 2026
Merged

fix(development): proper machine-list states, test the mobile sheet close#2049
2witstudios merged 4 commits into
masterfrom
pu/development-surface-polish

Conversation

@2witstudios

@2witstudios 2witstudios commented Jul 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace DevelopmentSidebar's single terse ListNotice string with the Machine page's shared SidebarLoading/SidebarNotice vocabulary (tab-states.tsx), so the machine list's loading/empty/failed states read like every other compact sidebar list in the app — in both drive-scoped and global (grouped-by-drive) mode.
  • The failed-load state now shows a Retry action wired to SWR's mutate, instead of a dead end.
  • A drive with zero visible machines is dropped from the API payload by design (listMachinesAcrossDrives), so there's no reachable "empty drive group" state within global mode — documented inline rather than built as unreachable UI.
  • Audited the mobile/narrow-viewport story end to end rather than re-implementing it:

Follow-up fixes (post-review)

  • Codex caught a real bug (thread): SidebarNotice's Retry button wired straight to onClick, so React handed it the click's MouseEvent — SWR's mutate treats a first argument as replacement cache data, not "revalidate now", so clicking Retry would have corrupted the machines cache instead of refetching. Fixed by wrapping both retryDriveMachines/retryAllMachines in a no-arg useCallback, matching DiffTab's existing SWR-backed Retry convention. Strengthened both retry tests to assert mutate was called with zero arguments — confirmed the assertion fails against the pre-fix code before restoring the fix.
  • Self-review turned up a second bug: the guard chain checked hasError && isEmpty ahead of isLoading, so a Retry click gave zero visible feedback — traced SWR's source (isLoading goes back to true on any revalidation where cached data is still undefined, exactly what a failed fetch leaves behind, while error stays stale until the new attempt settles). Reordered so loading wins over a stale error, and added a rerender-based regression test that simulates the actual retry-in-flight transition — confirmed it fails against the pre-fix ordering before restoring the fix.
  • Fixed the mutate-as-onClick bug at its root (apps/web/.../machine/tabs/tab-states.tsx — outside this PR's original file list, but the direct dependency this PR started consuming): the local useCallback workaround from the Codex fix above turned out to be the 4th independent hand-written copy of the same "wrap onAction so SWR's mutate doesn't get the click MouseEvent" fix — DiffTab, FilesFilePane/SettingsTab's reload, and FilesTab each already had one, and nothing stopped a 5th, 6th, 7th caller from forgetting it (TypeScript accepts SWR's mutate wherever onAction: () => void is declared — the mistake compiles clean). Fixed SidebarNotice/PaneNotice themselves to always call onAction() with zero arguments, verified safe for all 6 pre-existing call sites (108 tests, all green), simplified DevelopmentSidebar.tsx back down to passing mutate directly, and added tab-states.test.tsx — direct coverage pinning the guarantee at the component that now owns it.

Test plan

  • bun run vitest run src/components/layout/left-sidebar/__tests__/DevelopmentSidebar.test.tsx — 23 tests pass (loading/empty/error+retry × 2 modes, retry-arg-shape, retry-in-flight-shows-loading, sheet-close-on-navigate at narrow viewport, sheet-untouched at desktop width)
  • tab-states.test.tsx (new) — 2 tests pass, pinning the zero-arg guarantee directly
  • Full sweep of Development/Machine test files (21 files, 237 tests) — all pass
  • Full sweep of every tab-states.tsx consumer (DiffTab, FilesTab, SettingsTab, FilesFilePane, MachineFileTree — 108 tests) — all pass, confirming the root-cause fix doesn't regress any of the 6 pre-existing call sites
  • bun run typecheck (web) — clean
  • bun run lint (web) — clean (only pre-existing, unrelated warnings)
  • Every new regression test mutation-verified: reverted the corresponding fix, confirmed the test fails, restored the fix

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fzw6ieurR2FCLwbyT1wkRi

Summary by CodeRabbit

  • Bug Fixes
    • Improved machine list loading, empty, and error states in the Development sidebar.
    • Added retry actions when machine data fails to load.
    • Clarified empty-state messaging for drive-specific and global machine lists.
    • Improved sidebar behavior on mobile by automatically closing the sheet after selecting a machine.

…lose

DevelopmentSidebar's machine list had one generic ListNotice string for
every resting state. Replace it with the Machine page's shared
SidebarLoading/SidebarNotice vocabulary (tab-states.tsx) so loading, empty,
and failed states read like every other compact sidebar list in the app —
and give the failed-load state a Retry action wired to SWR's mutate, in
both drive-scoped and global (grouped-by-drive) mode.

A drive with zero visible machines is dropped from the API payload by
design (listMachinesAcrossDrives), so there's no reachable "empty drive
group" state to build for global mode — noted inline rather than added as
dead UI.

Audited the mobile story end to end: the sidebar already inherits
Layout.tsx's generic sheet treatment (it renders through MemoizedSidebar,
which Layout puts in a Sheet below the app's mobile breakpoint), the
Machine page's tab bar already went icon-only below `sm` in #2006 (before
the Development surface existed, so it was inherited for free), and
hover-revealed tree controls are covered by #2009's global touch-reveal
CSS. The one real gap was test coverage: the isSheetBreakpoint-driven
sheet-close-on-navigate wiring had no test. Added coverage for it, plus
the new loading/error/retry state branches, in both modes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzw6ieurR2FCLwbyT1wkRi
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

DevelopmentSidebar now uses shared loading and notice components for machine-list states, exposes SWR retry callbacks to drive and global lists, and adds tests for loading, empty, failure, retry, breakpoint, and sheet behavior.

Changes

Development sidebar state handling

Layer / File(s) Summary
Shared notice and retry flow
apps/web/src/components/layout/left-sidebar/DevelopmentSidebar.tsx
Machine queries expose retry callbacks, list components pass them to resolveListNotice, and shared sidebar components render loading, empty, admin, and failure states.
Sidebar state and interaction tests
apps/web/src/components/layout/left-sidebar/__tests__/DevelopmentSidebar.test.tsx
Tests cover drive and global loading, empty, failure, retry, breakpoint, and mobile-sheet behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DevelopmentSidebar
  participant MachineQuery
  participant MachineList
  participant SidebarNotice
  DevelopmentSidebar->>MachineQuery: capture mutate as retry callback
  DevelopmentSidebar->>MachineList: pass onRetry
  MachineList->>SidebarNotice: render list state
  SidebarNotice->>MachineQuery: invoke retry callback
Loading

Possibly related PRs

  • 2witstudios/PageSpace#2006: Introduces and standardizes the shared SidebarLoading and SidebarNotice components used by this sidebar refactor.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the two main changes: machine-list state handling and mobile sheet-close test coverage.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/development-surface-polish

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 343677d04a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

machines={machines}
isLoading={driveMachinesLoading}
error={driveMachinesError}
onRetry={retryDriveMachines}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wrap the retry mutate before handing it to the button

In an initial-load error, this SWR mutate is passed through to SidebarNotice, whose button uses it as onClick={onAction}; React will call it with the click MouseEvent, and SWR interprets that first argument as replacement cache data rather than a no-arg revalidation. The Retry button can therefore corrupt the machines cache instead of refetching (the global retryAllMachines path below has the same issue); wrap the call, e.g. onRetry={() => void retryDriveMachines()}.

Useful? React with 👍 / 👎.

2witstudios and others added 3 commits July 13, 2026 09:54
Codex review on #2049 caught a real bug: SidebarNotice's Retry button uses
onAction directly as the button's onClick, so React calls it with the
click's MouseEvent. SWR's mutate() interprets a first argument as
replacement cache data, not "revalidate now" -- so clicking Retry would
have handed a MouseEvent to mutate() and corrupted the machines cache
instead of refetching it, in both drive-scoped and global mode.

Wrap both call sites in a genuinely no-arg callback (mirroring how
DiffTab's own SWR-backed Retry already does this: void mutate(...) inside
a useCallback). Strengthened both retry tests to assert the mutate spy was
called with zero arguments, not just "called once" -- verified the new
assertion actually fails against the pre-fix code before re-applying the
fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzw6ieurR2FCLwbyT1wkRi
…n flight

Self-review (6-angle finder pass) surfaced a real UX gap introduced by the
Retry button added in the previous commit: resolveListNotice checked
`hasError && isEmpty` ahead of `isLoading`, so clicking Retry gave zero
visible feedback. Traced SWR's actual source (swr@2.4.1): isLoading is set
back to true on any revalidation where cached data is still undefined --
exactly what a failed fetch leaves behind -- while `error` stays at its
stale pre-retry value until the new attempt settles. Both are true at once
mid-retry, so checking error first meant the same "Failed to load
machines" text rendered throughout the retry, indistinguishable from the
click doing nothing.

Reordered: loading now wins over a stale error. Verified this doesn't
regress the background-poll-must-not-blank-a-good-list case (that path has
non-empty machines, so neither branch fires regardless of order) or the
cold-load case (error is never set yet). Added a rerender-based regression
test that simulates the actual retry-in-flight transition (not just a
static prop snapshot) and confirmed it fails against the pre-fix ordering
before restoring the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzw6ieurR2FCLwbyT1wkRi
….tsx

Self-review (4-angle simplify pass) flagged that the previous commit's
useCallback wrapper in DevelopmentSidebar.tsx fixed the "SWR mutate reads
the click MouseEvent as replacement cache data" bug at only one of what
turned out to be 7 call sites of SidebarNotice/PaneNotice's onAction
(DiffTab, FilesFilePane, SettingsTab, MachineFileTree, FilesTab x2, and now
DevelopmentSidebar). Every other call site already independently wrapped
its callback in a zero-arg closure to dodge the same bug -- three
hand-written copies of the same workaround, and nothing stops a future
caller from reintroducing it: TypeScript structurally accepts SWR's
mutate (or anything with an optional first parameter) wherever
onAction: () => void is declared, so the mistake compiles clean.

Fixed at the source instead: both SidebarNotice and PaneNotice now call
onClick={() => onAction()} rather than onClick={onAction}, so every
caller's zero-arg contract holds regardless of what onAction closes
over. Verified safe for all 6 pre-existing call sites (their onAction
callbacks were already effectively zero-arg) via the full consumer test
suite (108 tests, all green). Simplified DevelopmentSidebar.tsx's retry
wiring back down to passing SWR's mutate directly -- the local
useCallback workaround is no longer needed. Added tab-states.test.tsx,
direct coverage pinning the zero-arg guarantee at the component that
now owns it, and confirmed by reverting the fix that both the new
direct test and DevelopmentSidebar's retry tests fail without it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fzw6ieurR2FCLwbyT1wkRi
@2witstudios
2witstudios merged commit 1703005 into master Jul 13, 2026
3 checks passed
@2witstudios
2witstudios deleted the pu/development-surface-polish branch July 13, 2026 16:15
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