Sync sidebar organization, ordering, and navigation preferences through the server - #3304
Merged
SawyerHood merged 1 commit intoSep 9, 2026
Merged
Conversation
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 8, 2026 23:32
ba39d8d to
15a721c
Compare
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 8, 2026 23:42
15a721c to
d13e4c8
Compare
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 8, 2026 23:48
d13e4c8 to
fb9fd6e
Compare
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 9, 2026 17:03
fb9fd6e to
dbad21d
Compare
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 9, 2026 17:12
dbad21d to
23b23b8
Compare
SawyerHood
marked this pull request as ready for review
September 9, 2026 17:24
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 9, 2026 17:40
23b23b8 to
6bfab14
Compare
…gh the server Add a synced-preference atom that holds each UI preference in memory, hydrates from the server, and writes through a per-key queue with revision checks: queued edits compose as operations evaluated against the server value at the submitted revision, functional updates re-apply on conflict, cache updates stay revision-monotonic, and the atom reconciles to the cache when its queue drains. The sidebar waits for the preferences query alongside the project list instead of painting a default layout first. A one-shot migration uploads the value found in the old browser storage once when the server has none, then deletes that copy. Convert the organization mode, chronological sort, three section orders, navigation entry order and visibility, and both provider pickers, and stop persisting normalized orders on mount. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SawyerHood
force-pushed
the
bb/ui-preferences-client-sync
branch
from
September 9, 2026 18:31
6bfab14 to
f28dd5e
Compare
SawyerHood
added a commit
that referenced
this pull request
Sep 9, 2026
## Human comments ## What was wrong Every sidebar preference (organization mode, sort, section orders, collapsed rows, navigation entry order and visibility, provider pickers) lives only in browser localStorage, so a second window, device, or the CLI cannot see or change it, and there is no server surface to build sync on. The existing `app_settings_values` store has no revision and its `config-changed` broadcast invalidates thread timelines and provider queries, which is far too expensive for collapse-toggle frequency. ## What changed Layer 1 of 3 (stack #3306). Server foundation only; the app does not read these values yet. - `packages/domain/src/ui-preferences.ts`: typed registry of fifteen `sidebar.*` keys with zod schema, default, and description; `SidebarOrganizationMode` and `SidebarChronologicalSort` move here so app, server, SDK, and CLI share one source. - `packages/db`: new `ui_preferences` table (key, value_json, revision, updated_at) with migration `0115_ui_preferences`; data layer with `listStoredUiPreferences`, revision-checked `replaceStoredUiPreference`, and unchecked `overwriteStoredUiPreference` for reset. - `packages/server-contract` and `apps/server`: `GET /preferences/ui`, `PUT /preferences/ui/:key` with `expectedRevision` and `409 ui_preference_conflict`, `DELETE /preferences/ui/:key` resets to the default while advancing the revision. Unknown keys 404, schema failures 400. Stored values that no longer parse fall back to the default on read. - New `ui-preferences-changed` system change kind broadcast on every write; app realtime registry dirties only the new `uiPreferences` query; mobile registry compiles unchanged. - SDK: `sdk.system.uiPreferences.list()`, `.set()`, `.reset()`. CLI: `bb settings ui list|get|set|reset` (set reads the revision first and retries once on conflict). - App: `useUiPreferences` query, cache owner, and query key so later layers can hydrate from it. - Docs: new "Sidebar preferences" section in `docs/configuration.md`, guide template, and the bb-cli skill. No daemon wire change, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. The new domain types reach the plugin SDK's bundled types, so `@get-bb/plugin-sdk` is bumped to 0.4.51 to satisfy the npm version guard. ## Review One GPT-6 Astra review round requested changes; both findings are addressed: `SidebarOrganizationMode` and `SidebarChronologicalSort` are now re-exported from `@bb/domain` in the app instead of being redefined, and the docs, guide template, and bb-cli skill describe this layer as the server/SDK/CLI foundation with the browser sync explicitly deferred to #3304. ## How you verified - New `packages/db/test/data/ui-preferences.test.ts` (revision 0 create, stale-write conflict, overwrite advances revision). - New `apps/server/test/public/public-ui-preferences.test.ts` (defaults at revision 0, write and broadcast, 409 on stale write, 404 unknown key, 400 bad value, reset advances revision, unparseable stored value falls back). - Existing migrate tests updated to rewind the new table. - `pnpm exec turbo run typecheck --filter='...[origin/main]'` (79 tasks green) and `test` for `@bb/domain`, `@bb/db`, `@bb/server-contract`, `@bb/sdk`, `@bb/cli`, `@bb/templates`, plus the `@bb/server` public route suite and the app cache-owner and realtime tests. > AGENT GENERATED 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
SawyerHood
added a commit
that referenced
this pull request
Sep 9, 2026
## Human comments ## What was wrong After #3304 the structural sidebar settings sync, but the six collapsed-id lists (projects, threads, environments, built-in sections, thread sections, machines) still live only in localStorage. ## What changed Layer 3 of 3 (stack #3306). Base: #3304. - `sidebarCollapsedAtoms.ts`: the six collapsed atoms become in-memory synced preferences; their `atomWithStorage` wrappers and the folder-era storage adapter are deleted. Every existing toggle site already uses functional updates, so conflicting writes from two devices re-apply on top of the server list instead of clobbering it, and toggles made while a request is in flight compose into the next one. `RootComposeMobileRecents.test.tsx` seeds the atom through a jotai store instead of localStorage. - Ids of deleted entities are not pruned server-side. The client already drops a deleted project's id on the delete broadcast, a stale id renders nothing, and the registry caps each list at 10,000 entries. ## Review One GPT-6 Astra review round requested changes. All three findings were in the layer 2 sync controller (writes rebased onto the submitted revision, composed functional updates through a conflict, and reconciling the authoritative response after pending writes settle). They are fixed and tested in #3304. A later simplification pass removed this layer's server-side pruning and write debounce as not worth their code. ## How you verified - Sidebar, cache-owner, and mobile-recents suites plus the full `@bb/app`, `@bb/db`, and `@bb/server` public-route suites green; `pnpm exec turbo run typecheck --filter='...[origin/main]'` green. - Live in the dev app: clicking a project chevron in one window stores the id on the server and a fresh window opens with the row collapsed; a CLI write containing an unknown id is stored as sent. > AGENT GENERATED 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.
Human comments
What was wrong
With the server registry in place (#3303), the app still reads and writes the sidebar organization mode, chronological sort, the three section orders, navigation entry order and visibility, and the navigation and thread-list provider pickers from localStorage only. Separately,
usePersistedSidebarSectionOrderwrote the normalized order back on every mount where it differed, which is harmless for localStorage but would become a server PUT from every window whenever a project appears or disappears.What changed
Layer 2 of 3 (stack #3306). Base: #3303.
apps/app/src/lib/ui-preferences/:createSyncedPreferenceAtomis a plain in-memory jotai atom per key.UiPreferencesSync(mounted inApp) feeds theuiPreferencesquery into a controller that hydrates the atoms and writes through a per-key queue. Each queued edit is kept as an operation; functional updates compose in order and a plain value replaces the queue. The write path is one loop: read the base entry (cache or fetch), apply the operations to the server value at that revision, PUT withexpectedRevision; on a 409 refetch and try once more with migration operations dropped; on a second 409 adopt the server value. Cache updates from write responses are monotonic in revision, and once a key's queue drains it goes through the same reconcile as query data, so a broadcast that arrived mid-write still lands. Writes issued while one is in flight compose into the next request. Non-conflict failures toast once and invalidate the query.useUiPreferencesReadyinProjectList), showing the same loading state, so it never paints a default layout that then snaps. A failed query counts as ready with defaults.legacy-local-preferences.ts: the first time a client sees a key at revision 0 it reads the oldbb.sidebar.*browser value, validates it through the registry, uploads it withexpectedRevision: 0, and deletes the old keys (including the retired folder-era and hidden-panel keys, which are cleared but no longer rewritten). A losing migration never retries. This file is the only piece meant to be deleted a release or two later.sidebarOrganizationModeAtom,sidebarChronologicalSortAtom,sidebarSectionOrderAtom,sidebarManualSectionOrderAtom,sidebarMachineSectionOrderAtom,pluginNavPanelOrderAtom,pluginNavVisiblePanelKeysAtom,sidebarNavigationProviderAtom,threadListProviderAtom. Consumers keep the same atom interface. AllatomWithStoragewrappers, customSyncStorageadapters, legacy-key rewrites, and cross-windowstorageevent plumbing for these keys are deleted. A window that is hidden defers its refetch until it becomes visible, which is the app's existing fetch-suspension behavior; the old localStorage mirror had masked that for these keys.usePersistedSidebarSectionOrderonly derives the normalized order;PluginNavSidebarItemsno longer writes normalized order or visibility back on mount and persists the normalized order with the next user action instead.docs/configuration.mdand the guide template describe the wait-for-preferences behavior and the one-time upload.Review
One GPT-6 Astra review round requested changes; every finding is addressed in this revision with a regression test:
How you verified
apps/app/src/lib/ui-preferences/ui-preferences-sync.test.ts,legacy-local-preferences.test.ts, andUiPreferencesSync.test.tsx(readiness while pending, on data, on error): local-only without context, server value adopted over local state, legacy upload at revision 0 and not for defaults, migration never retried, cached revision write, list fetch when nothing cached, functional re-apply after conflict, plain-value retry after conflict, adopt-server after double conflict, coalescing plus reconcile skip, every queued update kept through a conflict, stale local state evaluated against the server value, queued update applied after a conflicting in-flight write, delayed response cannot regress a newer cache, broadcast adopted after the pending write, skip-equal write, toast-once on failure.PluginNavSidebarItems.test.tsxupdated so mounting persists nothing and user actions persist the normalized order; the obsoletesidebarCollapsedAtoms.migration.test.tsandpluginNavSidebarAtoms.test.tsare replaced by the legacy-migration tests.@bb/apptest suite green.pnpm exec turbo run typecheck --filter='...[origin/main]'green.bb.sidebar.*key and reloading uploads it once and clears every old key; a hidden window catches up when brought to front.🤖 Generated with Claude Code