Skip to content

Make the sidebar navigation a provider slot with rail and list placements - #5584

Merged
ymichael merged 19 commits into
mainfrom
bb/undo-left-rail-changes-thr_mgqwa2yese
Oct 16, 2026
Merged

ymichael merged 19 commits into
mainfrom
bb/undo-left-rail-changes-thr_mgqwa2yese

Conversation

@ymichael

@ymichael ymichael commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

#5229 (0.46.0) made the icon rail the only sidebar navigation and removed the plugin API that let a plugin draw it (experimental_sidebarNavigation, experimental_sidebarHeader, and their hooks).

The old slot only covered rows above the thread list, so the rail and the slot could not coexist.

What changed

One navigation slot with a declared placement.

  • app.slots.experimental_sidebarNavigation is restored under its pre-0.46 names. Registrations declare placement: "rail" | "list"; list is the default, so older plugins keep working.
  • bb lays out the sidebar from the placement before the provider renders:
    • rail: a fixed column that stays when the sidebar collapses, New thread in the header and Settings at the bottom of the column.
    • list: rows above the thread list, the footer in the same column after the thread list with a Settings action, Back to app rows on the Settings, Plugins, and Skills lists, a collapse that hides the whole sidebar, and the header slot.
  • Both placements use the same macOS window frame on wide viewports. Switching navigation keeps the title-bar controls in place and Settings Search at the same height; sidebar headers do not reserve an empty window-control gutter below the title bar. Providers without header controls use only an 8-pixel top inset instead of reserving an empty 48-pixel header row. Compact windows retain the floating toggle.
  • bb's Top list keeps the 0.45 row affordances: a hover "…" options button (always shown on touch devices) with the same menu as right-click, the split pane map beside a row's label when its page is open in a split, and More only when a destination is hidden.
  • bb's Top list pins New thread and More and scrolls the destinations between them, capped near 70% of the window with fades at the scroll edges, so many plugin panels or a short window can't push the thread list out of view (the pre-0.46 rows could). The shared region stays uncapped so plugin providers manage their own height.
  • bb's own Rail (navigation/rail, the default) and Top list (navigation/top-list) are the bundled Navigation plugin (see below), so they use exactly the contract a third-party provider gets.
  • The region is a <nav> element again, as before Make the navigation rail the only sidebar layout and remove the sidebar navigation plugin API #5229, and list placements keep the footer after the thread list in the same column, as 0.45 did. Plugins that find their column with closest("nav") and inset what follows, such as Nav Rail, therefore dock with the footer beside the rail.
  • Picking a provider (sidebar/sidebarNavigationProvider.ts):
    • sidebar.navigationProvider (Settings → Appearance → Navigation) selects Automatic or a provider by plugin key.
    • Automatic uses the first provider from a plugin the user installed, else the first built-in one (Rail). A pinned provider that is missing once plugins settle falls back the same way; with no provider at all the sidebar uses the list placement and says no navigation plugin is enabled.
    • Legacy __builtin__ and navigation/navigation read as Automatic.
    • Before plugin frontends settle, a plugin provider's last placement is reserved, so a reload does not flash the rail.
    • A crashing provider or header is replaced by the built-in provider for its placement, with one toast.
  • experimental_Original renders the built-in provider for the same placement, or nothing when the Navigation plugin is disabled.

Rail and Top list are a bundled, forkable plugin (plugins/navigation).

  • The plugin registers both providers, plus Rail's New thread button as its headerComponent, using only experimental_useSidebarNavigation, experimental_useSidebarNavigationSplit, experimental_SidebarNavigationIcon, and registry components. It is default-enabled, on scripts/forkable-plugins.json, and served by the demo server.
  • The host keeps only what surrounds a provider: the placement layout (including the rail's top inset), Settings, the customize editor, the navigation model, and badge collection. The macOS window frame is independent of navigation placement, and navigation headers sit below the title bar on wide macOS windows.
  • The shortcut pill, overflow fade, and split-pane mini-map move into shared-ui and the component registry, so the app, Navigation, and Thread list share one copy each (Thread list drops its private copies).

Built-in providers come from the install source.

  • The app records which plugins were installed as built-ins from the plugin list. Every replacement setting (thread list, navigation, source code, diffs) labels their providers "(built-in)" and lists them after installed plugins, and Automatic prefers a provider from an installed plugin, then slot order. No per-slot constants remain.
  • Monaco is a built-in plugin, so it now shows as "(built-in)" in the Source code and Diffs settings, and Automatic prefers a third-party renderer over it.
  • The bundled Thread list option drops its "BB default." description; the Automatic row already names what it chooses.
  • The Navigation setting hides when no navigation plugin is installed, like the thread list setting, and settings search uses the matching condition.

Shared model (SidebarNavigationModel.tsx, lib/plugin-sidebar-navigation.ts):

  • experimental_useSidebarNavigation() returns isHomeActive, and its actions include goHome(), setOrder(), and openCustomize(). openCustomize() opens bb's editor in a popover beside any provider, after open menus close, and returns focus to the control that opened it. bb's menus and the editor call it "Customize navigation" on every provider.
  • Each panel's experimental_useBadge value is in the model as item.badge. The badge sources mount once in the model instead of in the rail.

Badges (supersedes #5524).

  • This PR includes Draw plugin badges on navigation rail icons #5524's value-based experimental_useBadge and its compatibility handling for experimental_sidebarAccessory (the field is dropped with a warning), plus Tasks' migration to the hook.
  • ExperimentalBadge is { count?, text?, tone?, label }:
    • text is a short visible value of at most 8 characters, such as "NEW", "73%", or "2 down", and cannot be combined with count;
    • tone is neutral (the default), warning, or critical, and bb owns the colors.
  • The fields follow the 30 bb-community plugins that drew experimental_sidebarAccessory: 12 drew one count, 8 several numbers or a fraction, 5 a short status, 3 a percentage, and 18 used red, amber, or an accent color for state.
  • Drawing:
    • Fills are solid theme colors: muted gray for neutral, attention amber with a new --attention-foreground for warning, and destructive red for critical.
    • The rail draws counts (capped at 9+) and a tone-colored dot for text, each cut out of the icon by a ring in the rail's own background.
    • The Top list and More menu draw the count (capped at 99+), the text, or a dot.
    • More's dot takes the strongest hidden tone, and the label is in every accessible name.
  • Plugins written before 0.46 that drew counts from the item's old experimental_Accessory field show none until they read item.badge.

Header. A navigation's optional headerComponent renders in the sidebar header row while it is active, replacing the separate experimental_sidebarHeader slot and its sidebar.headerProvider setting; that slot remains as an untyped runtime alias so a pre-0.46 plugin's header joins its navigation. The thread list setting is now Sidebar thread list.

Plugin SDK 0.6.53.

  • Restored types and runtime exports.
  • ExperimentalBadgeTone and the new badge fields.
  • Collector validation for placement.
  • Test-harness support (the sidebarNavigation option and sidebarNavigationCalls).
  • Tasks now requires >=0.6.53. No server/daemon wire change.

Docs.

  • docs/configuration.md, the customization guide, and the plugin-authoring references (new frontend-sidebar-navigation.md, plus frontend-core-slots.md and the API indexes).
  • docs/api_to_audit.md entries with stabilization items and the core callers on the same path.
  • A Plugin Guide "Sidebar navigation" surface on the rail fixture, and verify-bb rows for Top list and navigation/header plugins, including disabling the Navigation plugin.
  • The navigation docs, SDK doc comments, and renderer slot docs describe the bundled Navigation plugin, the navigation/* keys, and the built-in-aware Automatic rule.

How you verified

  • Empty header correction (8cee6a05da): the new regression test fails before the fix; all 24 affected sidebar, header, and Settings tests pass after it, along with app lint and typecheck. The rendered macOS simulation confirms an 8-pixel inset above Top list and the retained 48-pixel Settings Search row.

  • Desktop spacing fix (fb133df5d9): both regression tests fail before the fix and pass after it. The 62 focused app layout, sidebar chrome/header, and Settings tests pass; app lint and typecheck pass. An isolated browser with simulated macOS chrome confirms Rail and Top list use identical title-bar/toggle coordinates and Settings Search height, with no extra window-control gutter. Compact windows retain their floating toggle. This renderer check does not verify native Electron window behavior.

  • Windows CI timeout fix (ae0adb0fa6): the two ACP restart tests use the same 30-second allowance as similar process-restart tests. All 499 ACP tests pass locally (4 skipped), along with lint and typecheck; Windows confirmation remains with CI.

  • Full typecheck, lint, and test run is left to CI on this branch (rebased onto 399d63d277). The rail and its Settings cell keep the spacing from Compact the macOS app frame around the traffic lights and move headers into the title bar #5529 (compact macOS frame) and Align sidebar rail and footer icons to the row rhythm #5609 (row-rhythm alignment), and the app typecheck passes after the rebase. Locally, the affected app, SDK, Plugin Guide, Tasks, guide, mobile, and server suites passed before the last rebase; the mobile theme and global.css were regenerated for the new --attention-foreground token.

  • check-plugin-sdk-version.mjs and check-npm-version-guard.mjs pass at 0.6.53.

  • Navigation plugin commit (5a0cc8d599):

    • scripts/check-plugin-forks.mjs plugins/navigation plugins/thread-list: both copies install, typecheck, test, and bb plugin build outside the monorepo with published packages only (Thread list passed on a re-run after one vitest worker start timeout under load).
    • Turbo typecheck and lint pass for the app, both sidebar plugins, shared-ui, plugin registry, server, demo server, domain, SDK, Plugin Guide, and templates. Tests pass for Navigation (14), Thread list, Plugin Guide, plugin registry, demo server (which now serves both sidebar plugins), the server's builtin-registry and UI-preference suites, and 229 app test files across settings, lib, sidebar, code, layout, and plugin. One app file (PluginSettings.test.tsx) failed under the parallel run and passed alone.
    • The app's Rail and Top list integration tests now load the real plugin through test/fixtures/navigation-plugin.ts; the plugin's own drawing (count caps, text badges, More's tone, menus, the header button) is tested portably in plugins/navigation/app.test.tsx.
    • After rebasing onto 399d63d277: turbo typecheck and lint pass for every package affected relative to main (155 tasks); Navigation, Thread list, plugin registry, and 231 app test files pass.
    • Ran pnpm start:worktree with the bundled plugin and the installed Nav Rail, Compact Nav, and Collapsible Navigation plugins. I confirmed the server and plugins come up, but did not repeat the visual pass below after this commit.
  • New and updated tests:

    • provider resolution (sidebarNavigationProvider.test.ts);
    • plugin providers on the real SDK path: list placement, badges, openCustomize, pinned built-ins, crash fallback, and Original (SidebarNavigationHost.test.tsx);
    • Top list rows, text and tone badges, the More dot, and Customize focus (SidebarNavigationList.test.tsx);
    • list placements keep the footer after the navigation in one column (AppSidebar.test.tsx);
    • the rail on the provider host, including invalid badge values (zero count, count plus text, text over 8 characters, unknown tone), text drawn as a toned dot, and More's strongest tone (SidebarNavigationRail.test.tsx);
    • the header slot, Back to app routing, and the last-known preference store;
    • SDK harness and placement validation, and the server reading stored pre-0.46 provider rows.
  • Manual checks with pnpm start:worktree in headless Chromium at 1440×900 (light and dark) and 390×844, plus the packaged macOS desktop app (pnpm desktop:worktree), with Rail and Top list on the framed and unframed windows:

    • Installed from bb-community, unmodified: Nav Rail, Compact Nav, and Collapsible Navigation. Each loads and draws its navigation; Nav Rail docks as a vertical rail with the footer beside it and moves New thread into the header; Compact Nav and Collapsible Navigation also work as header providers.
    • Installed the deleted examples/plugins/sidebar-navigation plugin: Customize, Original, the crash fallback, pinning Rail over it, a reload without a rail flash, and removal falling back to Rail.
    • A local plugin with four badged panels (critical text, warning count, warning text, neutral text) renders as intended on the rail and in the Top list in both themes.
  • Not verified: iOS Safari and the native mobile shell; the header alignment on macOS after the header moved into the plugin. A pinned plugin key that is not installed shows an empty rail-width column on the first load until frontends settle, then falls back.

Follow-up commits on this branch.

  • fb133df5d9 keeps the macOS window chrome stable when switching between rail and list placements (AppLayout.tsx, SidebarChrome.tsx, with tests and docs).
  • 8cee6a05da removes the empty header row above Top list navigation (AppSidebar.tsx, with a test).
  • ae0adb0fa6 gives the ACP restart tests enough time on Windows (provider-bridge-acp-next test timeouts only).
  • CI is green on 8cee6a05da: 48 checks passed or skipped.

Supersedes #5524.

Fixes #5360
Fixes #5327

🤖 Generated with Claude Code

AGENT GENERATED

@ymichael
ymichael force-pushed the bb/undo-left-rail-changes-thr_mgqwa2yese branch 2 times, most recently from 2bd7d5c to e462ac6 Compare October 16, 2026 03:56
ymichael and others added 16 commits October 16, 2026 11:56
…ents

bb draws the sidebar navigation through app.slots.experimental_sidebarNavigation
again, restored under its pre-0.46 names so released navigation plugins load
unmodified. Registrations declare a placement: rail (a fixed column that stays
when the sidebar collapses, with the framed macOS title bar) or list (rows above
the thread list, Settings in the footer, Back to app rows, and the header slot).
bb ships two providers through the same contract, Rail and Top list, picked
with sidebar.navigationProvider in Settings -> Appearance -> Navigation;
Automatic prefers an installed plugin provider and falls back to Rail.

The navigation model now carries Home, Customize, reorder, and each panel's
experimental_useBadge value (folded in from #5524), so every provider draws the
same badges. experimental_sidebarHeader returns for list placements. Plugin
SDK 0.6.52.

Fixes #5360
Fixes #5327

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The navigation region is a nav element again, as it was before #5229, so
plugins that find their column with closest("nav"), such as Nav Rail, dock as
designed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExperimentalBadge gains text (a short visible value, at most 8 characters,
exclusive with count) and tone (neutral, warning, or critical, drawn with bb's
theme colors). The Top list and More menu draw the count or text in a pill
colored by tone; the icon-only rail draws counts and a tone-colored dot for
text. More's dot takes the strongest hidden tone. The shape follows the 30
bb-community plugins that drew experimental_sidebarAccessory: counts, short
statuses, percentages, and fractions, with red, amber, or neutral colors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Badges use solid fills from the theme: a muted gray for neutral, attention
amber with a new --attention-foreground for warning, and destructive red for
critical. On the rail every count and dot sits in a ring painted with the
rail's own background, so it reads against the icon instead of over it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Top list pins New thread and More and scrolls the destinations between
them, capped near 40% of the window, with fades while more rows sit above or
below. Before, enough plugin panels or a short window pushed the thread list
out of view and clipped More, as the pre-0.46 rows also did. The shared
navigation region stays uncapped so plugin providers keep managing their own
height.

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

Top list rows get the 0.45 hover "…" button again, with the same menu as
right-click (shared with the rail's context menu), always shown on touch
devices; a badge fades out while the button shows in its place. Rows draw the
split pane map beside the label when their page is open in a split. More only
appears when a destination is hidden, since each row's menu reaches Customize.
shared-ui now exports its sidebar hover-action classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rail said "Customize rail" and the Top list said "Customize sidebar",
but the editor only picks which destinations show and in what order. One
name now covers the menu items and the editor title on both providers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list layout put the footer in the bottom grid row the rail uses, outside
the column that holds the navigation and thread list. Docking plugins such
as Nav Rail inset the elements after the navigation in that column, as they
did in 0.45, so they missed the footer: Settings and the footer actions sat
under the rail at the window's edge and the rail's tint stopped above them.
List placements now render the footer after the thread list in the same
column; only the rail keeps the separate row beside its Settings button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings -> Appearance called it "Header", which read as any header in the
app. It now matches the "Sidebar footer" setting below it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Sidebar" alone read as the whole sidebar, beside separate Sidebar header
and Sidebar footer settings. It names the part it changes now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rail keeps New thread in the sidebar header, so a header provider never
mounts there. The setting now says so and is disabled instead of offering a
choice that has no effect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidebar header was a separate exclusive slot with its own setting, so
users could pair a header with a navigation it did not belong to, and the
setting did nothing with a rail. A navigation registration now takes an
optional headerComponent that renders beside the sidebar toggle while that
navigation is active. bb's Rail supplies New thread as its header through
the same resolution, and a crashing plugin header falls back to the header
of bb's provider for the same placement.

experimental_sidebarHeader stays as an untyped runtime alias: a plugin's
header joins each of its navigations without a headerComponent, so Nav
Rail, Compact Nav, and Collapsible Navigation keep working. The Sidebar
header setting and the sidebar.headerProvider preference are removed.

Top list draws New thread as a regular row again, as in 0.45: it follows
the saved order and visibility and appears in Customize. The rail keeps it
pinned in its header and out of Customize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bb's two sidebar navigation layouts now ship as the bundled, default-enabled
Navigation plugin (plugins/navigation), registered through
experimental_sidebarNavigation like any other provider and kept on the
forkable list. The host keeps only the placement layout, Settings, the
customize editor, and the navigation model.

Whether a provider is built-in now comes from the plugin's install source
instead of per-picker constants, so every replacement slot labels built-in
providers "(built-in)", lists them after installed plugins, and prefers an
installed plugin under Automatic. Shortcut pill, overflow fade, and the split
pane mini-map move into shared-ui and the component registry so the app and
both sidebar plugins share one copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymichael
ymichael force-pushed the bb/undo-left-rail-changes-thr_mgqwa2yese branch from 1d50964 to 5a0cc8d Compare October 16, 2026 18:58
@ymichael
ymichael merged commit 3e7e490 into main Oct 16, 2026
48 checks passed
@ymichael
ymichael deleted the bb/undo-left-rail-changes-thr_mgqwa2yese branch October 16, 2026 20:43
@yusuf8834

Copy link
Copy Markdown

when top list enabled. right and left edges are not equal.

image

ymichael added a commit that referenced this pull request Oct 17, 2026
## Human comments

## What was wrong

Follow-up to #5584 (reported in
#5584 (comment): "when
top list enabled, right and left edges are not equal"). In the framed
macOS desktop window, the window frame left an 8px lip on the right and
bottom of the card. With the Rail placement, the rail fills the left
side. With the list placement (Top list) there is no rail, and the frame
had no leading lip, so the sidebar card (and the content card when the
sidebar is collapsed) sat flush against the window's left edge. The
collapsed title-bar reserve also assumed a rail was present, so the page
title overlapped the sidebar toggle in Top list.

## What changed

- `apps/app/src/components/ui/sidebar.tsx`, `apps/app/src/app.css`: with
the list placement, the frame gains a leading lip
(`--bb-window-frame-leading-lip`), so the sidebar card and the collapsed
content card are inset 8px on the left, matching the right and bottom.
`SidebarProvider` publishes `--bb-window-card-collapsed-left`: the rail
width for Rail, the leading lip for Top list.
- `apps/app/src/lib/bb-desktop.ts`:
`MACOS_TITLE_BAR_COLLAPSED_SIDEBAR_RESERVE_CLASS` subtracts
`--bb-window-card-collapsed-left` instead of assuming a rail, so the
title clears the toggle when the sidebar is collapsed in Top list.
- Rail placement and non-macOS or unframed windows are unchanged. No
wire, CLI, or plugin API changes.

## How you verified

- Added cases to `apps/app/src/components/ui/sidebar.test.tsx` for the
leading lip and the published collapsed-left offset under both
placements.
- `pnpm exec turbo run typecheck lint --filter=@bb/app` and the
sidebar/layout/secondary-panel tests pass.
- Measured in the real Electron app via CDP, window 1280px wide:
  - Top list open: left gap 0 -> 8px; right and bottom gaps stay 8px.
- Top list collapsed: left gap 0 -> 8px; the title now clears the
sidebar toggle.
  - Rail open and collapsed: unchanged.

> AGENT GENERATED

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants