Skip to content

fix(app): your sidebar layout survives the agent restarting as the app starts (BRA-3095) - #215

Draft
Stener1 wants to merge 1 commit into
cosscom:mainfrom
Stener1:bra-3095-sidebar-layout-persist
Draft

Stener1 wants to merge 1 commit into
cosscom:mainfrom
Stener1:bra-3095-sidebar-layout-persist

Conversation

@Stener1

@Stener1 Stener1 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

After an update, the sidebar sometimes came back as the default layout: Customize sidebar's arrangement looked lost. The layout is read once, at connect, from the laptop agent (GET /v1/app/sidebar). If that read failed, for example because the agent was restarting while the updated app started, the app kept the default for the whole launch and marked it loaded. The next change then saved the default over sidebar.json, which made the reset permanent.

 connect
   loadNav()
-    GET /v1/app/sidebar  ── fails ──> layout = default, loaded = true   (never read again)
+    GET /v1/app/sidebar  ── fails ──> layout = default, loaded = false
+      poll(): again after 1 s, doubling to 10 s, not while hidden
+      event stream reconnects ──> retryNav(): try now
+    ── succeeds ──> layout = stored, loaded = true   (a missing or empty file is the default)

 change (Customize sheet, right-click, drag, ⌘K)
-  save(default + change)                       // could write over the stored file
+  navReady()? no ──> toast "Your saved layout hasn't loaded yet. Trying again…", nothing saved
+  yes ──> save, one at a time (oneAtATime), keeping unavailable ids in place

A failed read is now tried again until the stored layout arrives, and nothing can change the layout before that. More's Customize sidebar… is disabled with a note. Any other change shows the same note as a toast.

The PR also fixes two smaller problems on the same write path:

  • Places that weren't available were dropped on save. place and move saved only the ids available at that moment. A pinned plugin that was off or still loading lost its place the next time anything else changed. Those ids now stay at their stored index.
  • A failed save could undo a later one. Saves ran concurrently, so a write that failed could roll back over one that had succeeded after it (Codex adversarial review). Saves now run in turn.
app/src/lib/
├── nav-core.ts        # arrange / migrate / place / move / navSync: pure, node-tested
├── nav-core.test.ts   # new
└── nav.ts             # wires navSync to the agent; loadNav (poll), retryNav, navReady

The file format doesn't change, and no Go code changes.

Same shape elsewhere, not changed here: loadProjects (lib/project-groups.ts) also marks itself loaded after a failed read and never retries, so project sections can be missing for one launch. It can't overwrite the file the way the sidebar did, because saveProjects re-reads the document before it writes. Its fallback "write what this window has" only applies when that re-read also fails, and then the PUT almost certainly fails too. A shared /v1/app/<key> sync could cover both later.

Evidence

Real agent (scripts/sidebar-layout-restart.sh). The script seeds sidebar.json with Kits pinned and Automations hidden. It runs a laptop agent of this checkout with an empty box, and stops the agent the moment the app sends GET /v1/app/sidebar. The agent starts again 2 s later.

  • Before (origin/main's build): after the restart the app stays on the default layout. Kits isn't pinned and Automations is back.
    before
  • After (this branch): the read fails (sidebar layout: The app lost its connection to the agent on this computer… in the console). It is tried again, and the stored layout appears.
    after
    sidebar.json is byte-identical afterwards:
    {"ok":true,"sidebar_json_before":"4f21c652…af48","sidebar_json_after":"4f21c652…af48"}

This reproduces the failure on a real agent restart that I timed into the read. That an update's restart actually lands in that window on a Mac is still the likely trigger, not a proven one: it is intermittent, and the app's other first reads succeeded in the reported launch.

Before the layout has loaded (mock, drive.mjs sidebar-layout-reload, ?failapp=sidebar).

  • More's Customize sidebar… is disabled, with the note:
    blocked
  • Hide on a row's right-click is refused with the same note, and the stored document is unchanged:
    refused
  • A pin survives a reload:
    reload

Tests:

  • e2e (e2e/sidebar-layout-load.spec.ts, new, passing): a pin survives a reload. The route fails, so the default shows, Customize is blocked and a change is refused. The route recovers, the stored layout appears and Customize opens. plugin-nav-badges and rail still pass.
  • unit (nav-core.test.ts, 15 tests): arrange and migrate. A save with a plugin id unavailable keeps that id's place. A save before a successful load is refused and writes nothing. A missing file counts as loaded. Saves run in turn, so an earlier failed write can't undo a later one.

Checks run: pnpm test (643), tsc, tsc -p e2e, check:titles, check:type, check:grid, check:csp, pnpm build and the docs build. Not run: go vet/go test (no Go changes) and cargo check (no Rust changes).

Verify skill:

  • features/sidebar-nav.md gains nav-layout-load.
  • New scenarios: sidebar-layout-reload and sidebar-layout-restart-live.
  • New scripts: scripts/agent-box.sh (a laptop agent with an empty box, on ports 9467/9468/7546) and scripts/sidebar-layout-restart.sh.
  • The mock keeps the sidebar document in session storage across reloads. ?failapp=sidebar fails it until __mockApp.recover("sidebar").

Merge Danger

Door: two-way

The only side effect is ~/.berth/app/sidebar.json, and its format is unchanged, so reverting leaves a valid file. Ids kept for unavailable plugins are already skipped by arrange() at render.

Blast Radius: sidebar

The changes are all in the sidebar's places: the list, the rail and zen's switcher. Customize sidebar can't be used until the layout loads; if the agent never answers, it stays disabled with a note while the app keeps retrying. A first-run user with no file isn't affected: a missing file counts as loaded.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

View guided diff

…p starts

The sidebar's layout read once at connect; a failed read left the default
showing for the launch, and the next change saved the default over the
stored file. The read is now tried again (lib/poll, and at once on a
reconnect) until it arrives, and nothing can change the layout before then:
Customize sidebar… waits with a note, and other changes are refused with
the same note. A change also keeps the place of pinned or hidden places that
aren't available (a plugin off or still loading), and saves run one at a
time, so a write that fails can't roll back a later one.

The verify skill gains sidebar-layout-reload (mock) and
sidebar-layout-restart.sh, which restarts a real laptop agent as the app
asks for the layout and checks sidebar.json is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 2, 2026

Copy link
Copy Markdown

@Stener1 is attempting to deploy a commit to the seanbrydonio's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Stener1

Stener1 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Stener1 can't add labels here: could a maintainer add run-ci? The app checks, pnpm test, pnpm build, the docs build and the touched e2e specs pass locally (see Evidence).

This branch has not been deployed

No deployments
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