Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
25c50d9
Make the sidebar navigation a provider slot with rail and list placem…
ymichael Oct 15, 2026
348292a
Keep pre-0.46 navigation plugins docking in the sidebar column
ymichael Oct 15, 2026
b40946b
Let plugin badges show short text and a tone
ymichael Oct 15, 2026
0409fa6
Draw navigation badges as solid tone fills cut out of the rail
ymichael Oct 15, 2026
465430f
Bump the plugin SDK to 0.6.53 for the restored navigation API and bad…
ymichael Oct 15, 2026
287fa4f
Scroll Top list destinations so they cannot crowd out the thread list
ymichael Oct 15, 2026
b5cf342
Let Top list destinations take up to 70% of the window before scrolling
ymichael Oct 15, 2026
9b904a6
Bring back Top list row options, the split map, and More only when ne…
ymichael Oct 15, 2026
c2f51ee
Call the navigation editor "Customize navigation" on every provider
ymichael Oct 16, 2026
a0b212b
Keep the sidebar footer in the navigation column for list placements
ymichael Oct 16, 2026
d383bba
Label the sidebar header setting "Sidebar header"
ymichael Oct 16, 2026
6cdf8dd
Explain the sidebar header setting in plain terms
ymichael Oct 16, 2026
478e8c8
Label the thread list setting "Sidebar thread list"
ymichael Oct 16, 2026
4bb8e8e
Disable the sidebar header setting while the navigation is a rail
ymichael Oct 16, 2026
da83ba2
Register the sidebar header with its navigation
ymichael Oct 16, 2026
5a0cc8d
Make Rail and Top list a forkable bundled Navigation plugin
ymichael Oct 16, 2026
ae0adb0
Allow ACP restart tests enough time on Windows
ymichael Oct 16, 2026
fb133df
Keep macOS window chrome stable across navigation placements
ymichael Oct 16, 2026
8cee6a0
Remove the empty header row above Top list navigation
ymichael Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .bb/skills/verify-bb/features/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ command’s `--help` before mutation. Use fresh browser snapshots for controls.
| Usage | Inspect Usage limits and settings usage; compare provider-reported windows. | Unavailable data remains unavailable rather than zero. |
| AI services picker | Open Settings → AI services. For Thread titles and Commit messages pick Automatic, a registered service, and Off; press Test; mirror with `settings ai-services`, `settings ai-services set <task> <choice>`, and `settings ai-services test <task>`. Create a thread and use the Commit action under each choice. | Each row lists only services that handle the task, with their status line; Automatic names the first ready builtin service; a picked service is used alone and a failure falls back to prompt-text titles and `bb: automated commit`; Off never calls a service; a failed Test shows the reason in the row; choices persist across reload and match the CLI. |
| Experiments | Exercise changelogPreview and serverMove on/off in isolated data. | Only the named feature gate changes; disabled routes/actions fail or disappear as designed; state restores. |
| Navigation rail | Open Settings, Plugins, Skills, and a plugin panel from the rail on a wide window and in the compact drawer; confirm the rail stays, the matching destination is current, and Home returns to the last thread. Hide and reorder a destination through More → Customize rail. | Rail order and visibility persist in `sidebar.pluginPanelOrder` and `sidebar.visiblePluginPanels`; in the drawer, Home, Plugins, Skills, and Settings leave it open and a plugin page closes it. |
| Navigation rail | Open Settings, Plugins, Skills, and a plugin panel from the rail on a wide window and in the compact drawer; confirm the rail stays, the matching destination is current, and Home returns to the last thread. Hide and reorder a destination through More → Customize navigation. | Rail order and visibility persist in `sidebar.pluginPanelOrder` and `sidebar.visiblePluginPanels`; in the drawer, Home, Plugins, Skills, and Settings leave it open and a plugin page closes it. |
| Top list navigation | Pick Settings → Appearance → Navigation → Top list (or `settings ui set sidebar.navigationProvider navigation/top-list`) on a wide window and in the compact drawer; open Plugins, Settings from the footer, and a plugin panel; collapse the sidebar; hide a destination and New thread through More → Customize navigation, then move New thread below Plugins; reload. | No rail; on macOS desktop, the title bar keeps the same controls and Settings search stays at the same height as Rail; labeled rows sit above the thread list with the current destination marked; Settings, Plugins, and Skills lists start with Back to app; collapsing hides the whole sidebar; a nav panel badge shows at the end of its row; the layout survives reload without a rail flash and matches the CLI. |
| Navigation plugins | Install a plugin registering `experimental_sidebarNavigation` (once with `placement: "list"` and a `headerComponent`, once `"rail"`); leave Navigation on Automatic, then pin Rail; make the provider and its header throw; disable the plugin; reload with the plugin pinned; finally disable the bundled Navigation plugin too. Repeat with a plugin built for SDK 0.45 that calls `experimental_sidebarHeader` separately. | Automatic switches to the plugin with the layout of its placement; the header shows beside the sidebar toggle only while its list navigation is active; pinning Rail restores the Navigation plugin's rail and New thread in the header; a crash draws the Navigation plugin's provider for that placement (or its header) with one toast; disabling falls back to Rail; the reload reserves the plugin's placement without a rail flash; the legacy header joins its plugin's list navigation; with both disabled the sidebar uses the list layout, says no navigation plugin is enabled, and keeps Settings in the footer. |
| Storage & retention plugin | Enable bb--storage-retention in a fresh instance. Verify Never/off defaults, immediate saves without success toasts, failed-save rollback, reload persistence, CLI parity, scans, background cleanup progress, and inline deletion confirmations. Using disposable fixtures, verify archive storage cleanup after the undo grace, cancellation on unarchive/pin, and development cleanup after checkout removal, including offline/reload recovery. | Automatic cleanup requires the corresponding opt-in. Clearing files preserves conversations and attachment originals; large-file cleanup preserves small files. Pins exempt affected groups; automation protection is deferred. Removal-feed cursors and pending work survive reload; expired cursors trigger reconciliation. Disabling stops plugin schedules and actions; re-enabling preserves policy. Verify compact Safari without app-root inert/aria-hidden. |
| Debug events | Toggle Show unhandled provider events and render a trusted unsupported-event fixture. | The diagnostic row visibility follows the toggle without changing persisted event data. |
| Community and update surfaces | Open Community links, version/update view, changelog, and CLI skills status. | Destinations and installed/latest status are correct; viewing does not perform an update. |
Expand Down
1 change: 1 addition & 0 deletions apps/app/.ladle/story-settings-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export function SettingsStoryChrome({
>
<Sidebar>
<SettingsSidebarContent
backTo={null}
isResizing={false}
navigation={{
activePluginId: null,
Expand Down
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"bb-plugin-environment-modal-sandbox": "workspace:*",
"bb-plugin-provider-pi": "workspace:*",
"bb-plugin-secrets": "workspace:*",
"bb-plugin-navigation": "workspace:*",
"bb-plugin-thread-list": "workspace:*",
"cacache": "^20.0.4",
"fake-indexeddb": "^6.2.5",
Expand Down
6 changes: 3 additions & 3 deletions apps/app/src/components/code/codeRendererProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useAtomValue } from "jotai";
import {
createReplacementPreferenceAtom,
resolvePreferredReplacement,
usePreferredReplacement,
} from "@/lib/plugin-replacement-preference";
import type { ResolvedReplacement } from "@/lib/plugin-slot-resolvers";
import {
Expand All @@ -24,11 +24,11 @@ export const diffRendererProviderAtom = createReplacementPreferenceAtom(
export function useSourceCodeRendererReplacement(): ResolvedReplacement<PluginSourceCodeRendererSlot> {
const { sourceCodeRenderers } = usePluginSlots();
const preference = useAtomValue(sourceCodeRendererProviderAtom);
return resolvePreferredReplacement(sourceCodeRenderers, preference);
return usePreferredReplacement(sourceCodeRenderers, preference);
}

export function useDiffRendererReplacement(): ResolvedReplacement<PluginDiffRendererSlot> {
const { diffRenderers } = usePluginSlots();
const preference = useAtomValue(diffRendererProviderAtom);
return resolvePreferredReplacement(diffRenderers, preference);
return usePreferredReplacement(diffRenderers, preference);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
SIDEBAR_STANDARD_ROW_PADDING_CLASS,
} from "@bb/shared-ui/sidebar-row-classes";
import { StoryCard, StoryRow } from "../../../.ladle/story-card";
import { AppCommandShortcutPill } from "./AppCommandShortcutHint";
import { ShortcutPill } from "@bb/shared-ui/shortcut-pill";

export default {
title: "commands/Shortcut Hint",
Expand Down Expand Up @@ -46,7 +46,7 @@ function ShortcutSet({
return (
<span className="inline-flex flex-wrap items-center gap-1.5">
{shortcuts.map((item) => (
<AppCommandShortcutPill key={item.ariaKeyshortcuts} shortcut={item} />
<ShortcutPill key={item.ariaKeyshortcuts} shortcut={item} />
))}
</span>
);
Expand Down Expand Up @@ -88,7 +88,7 @@ function TextBaselineExample({
<CenterlineFrame>
<span className={cn("inline-flex items-baseline gap-1.5", className)}>
<span>Open panel</span>
<AppCommandShortcutPill shortcut={shortcut} />
<ShortcutPill shortcut={shortcut} />
</span>
</CenterlineFrame>
);
Expand All @@ -100,7 +100,7 @@ function ToolbarExample() {
<Button variant="ghost" size="sm" className="h-7 gap-1.5 px-2">
<Icon name="MessageSquarePlus" />
<span>New thread</span>
<AppCommandShortcutPill shortcut={MAC_SHORTCUTS[0]} />
<ShortcutPill shortcut={MAC_SHORTCUTS[0]} />
</Button>
<Button
variant="ghost"
Expand All @@ -110,7 +110,7 @@ function ToolbarExample() {
>
<Icon name="Search" />
</Button>
<AppCommandShortcutPill shortcut={MAC_SHORTCUTS[1]} />
<ShortcutPill shortcut={MAC_SHORTCUTS[1]} />
</div>
);
}
Expand Down Expand Up @@ -138,7 +138,7 @@ function SidebarRowExample({
Storybook pass for shortcut alignment
</span>
<span className="flex shrink-0 items-center gap-0.5">
<AppCommandShortcutPill shortcut={shortcut} />
<ShortcutPill shortcut={shortcut} />
</span>
</div>
</div>
Expand All @@ -154,8 +154,8 @@ export function Overview() {
<ShortcutSet shortcuts={WIN_SHORTCUTS} />
</StoryRow>
<StoryRow label="long chord">
<AppCommandShortcutPill shortcut={MAC_SHORTCUTS[2]} />
<AppCommandShortcutPill shortcut={WIN_SHORTCUTS[2]} />
<ShortcutPill shortcut={MAC_SHORTCUTS[2]} />
<ShortcutPill shortcut={WIN_SHORTCUTS[2]} />
</StoryRow>
</StoryCard>

Expand Down
28 changes: 2 additions & 26 deletions apps/app/src/components/commands/AppCommandShortcutHint.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cn } from "@bb/shared-ui/lib/utils";
import { ShortcutPill } from "@bb/shared-ui/shortcut-pill";
import type { AppShortcutPresentation } from "@/lib/app-keybindings";
import { useIsAppCommandModifierHeld } from "./AppCommandProvider";

Expand All @@ -7,36 +7,12 @@ interface AppCommandShortcutHintProps {
className?: string;
}

interface AppCommandShortcutPillProps {
ariaHidden?: boolean;
shortcut: Pick<AppShortcutPresentation, "label">;
className?: string;
}

const APP_COMMAND_SHORTCUT_HINT_CLASS =
"pointer-events-none inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-sm bg-state-hover px-1.5 py-1 font-sans text-xs font-normal leading-none tabular-nums text-subtle-foreground opacity-60";

export function AppCommandShortcutPill({
ariaHidden = true,
shortcut,
className,
}: AppCommandShortcutPillProps) {
return (
<kbd
aria-hidden={ariaHidden}
className={cn(APP_COMMAND_SHORTCUT_HINT_CLASS, className)}
>
{shortcut.label}
</kbd>
);
}

export function AppCommandShortcutHint({
shortcut,
className,
}: AppCommandShortcutHintProps) {
const isPrimaryModifierHeld = useIsAppCommandModifierHeld();
if (!isPrimaryModifierHeld || !shortcut) return null;

return <AppCommandShortcutPill shortcut={shortcut} className={className} />;
return <ShortcutPill shortcut={shortcut} className={className} />;
}
111 changes: 71 additions & 40 deletions apps/app/src/components/layout/AppLayout.plugin-panel-header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@ import {
setCompactSecondaryPanelPresentation,
} from "@/components/ui/secondary-panel-shelf-visibility";

const viewportState = vi.hoisted(() => ({
compact: false,
macosChrome: false,
}));
const viewportState = vi.hoisted(
(): {
compact: boolean;
macosChrome: boolean;
navigationPlacement: "rail" | "list";
} => ({
compact: false,
macosChrome: false,
navigationPlacement: "rail",
}),
);

vi.mock("@bb/shared-ui/hooks/use-compact-viewport", () => ({
useIsCompactViewport: () => viewportState.compact,
Expand Down Expand Up @@ -58,9 +65,20 @@ vi.mock("@/hooks/usePluginSafeModeCommands", () => ({
}));

vi.mock("@/lib/plugin-slots", () => ({
useBuiltinPluginIds: () => new Set<string>(),
usePluginSlots: () => ({
appOverlays: [],
commandPaletteActions: [],
experimentalSidebarNavigations: [
{
pluginId: "navigation",
id: "test-navigation",
title: "Navigation",
placement: viewportState.navigationPlacement,
component: () => null,
generation: 1,
},
],
fileOpeners: [],
navPanels: [
{
Expand Down Expand Up @@ -210,6 +228,7 @@ describe("AppLayout plugin panel header", () => {
beforeEach(() => {
viewportState.compact = false;
viewportState.macosChrome = false;
viewportState.navigationPlacement = "rail";
setCompactSecondaryPanelPresentation("closed");
});

Expand Down Expand Up @@ -284,42 +303,54 @@ describe("AppLayout plugin panel header", () => {
).toBeUndefined();
});

it("moves the trigger and history controls into a window title bar beside the macOS rail", () => {
viewportState.macosChrome = true;
renderPluginPanelRoute();

const titleBar = screen.getByTestId("app-window-title-bar");
expect(screen.getByTestId("app-layout-root").dataset.framed).toBe("");
expect(
titleBar.contains(
screen.getByRole("button", { name: /^Toggle sidebar/ }),
),
).toBe(true);
expect(
titleBar.contains(screen.getByRole("button", { name: "Go back" })),
).toBe(true);
expect(screen.queryByTestId("app-desktop-sidebar-trigger")).toBeNull();
});

it("keeps the sidebar toggle in the rail column and no title bar on wide web, Windows, and Linux windows", () => {
renderPluginPanelRoute();

expect(screen.queryByTestId("app-window-title-bar")).toBeNull();
expect(screen.getByTestId("app-sidebar-trigger-overlay")).toBeTruthy();
expect(
screen.getByTestId("app-layout-root").dataset.framed,
).toBeUndefined();
});
it.each(["rail", "list"] as const)(
"keeps the macOS trigger and history controls in the window title bar with %s navigation",
(placement) => {
viewportState.navigationPlacement = placement;
viewportState.macosChrome = true;
renderPluginPanelRoute();

const titleBar = screen.getByTestId("app-window-title-bar");
expect(screen.getByTestId("app-layout-root").dataset.framed).toBe("");
expect(
titleBar.contains(
screen.getByRole("button", { name: /^Toggle sidebar/ }),
),
).toBe(true);
expect(
titleBar.contains(screen.getByRole("button", { name: "Go back" })),
).toBe(true);
expect(screen.queryByTestId("app-desktop-sidebar-trigger")).toBeNull();
},
);

it("keeps the floating macOS trigger on compact windows", () => {
viewportState.macosChrome = true;
viewportState.compact = true;
renderPluginPanelRoute();
it.each(["rail", "list"] as const)(
"keeps the floating sidebar toggle on wide web, Windows, and Linux windows with %s navigation",
(placement) => {
viewportState.navigationPlacement = placement;
renderPluginPanelRoute();

expect(screen.queryByTestId("app-window-title-bar")).toBeNull();
expect(screen.getByTestId("app-sidebar-trigger-overlay")).toBeTruthy();
expect(
screen.getByTestId("app-layout-root").dataset.framed,
).toBeUndefined();
},
);

expect(screen.queryByTestId("app-window-title-bar")).toBeNull();
expect(screen.getByTestId("app-desktop-sidebar-trigger")).toBeTruthy();
expect(
screen.getByTestId("app-layout-root").dataset.framed,
).toBeUndefined();
});
it.each(["rail", "list"] as const)(
"keeps the floating macOS trigger on compact windows with %s navigation",
(placement) => {
viewportState.navigationPlacement = placement;
viewportState.macosChrome = true;
viewportState.compact = true;
renderPluginPanelRoute();

expect(screen.queryByTestId("app-window-title-bar")).toBeNull();
expect(screen.getByTestId("app-desktop-sidebar-trigger")).toBeTruthy();
expect(
screen.getByTestId("app-layout-root").dataset.framed,
).toBeUndefined();
},
);
});
18 changes: 13 additions & 5 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import { useFaviconBadge } from "@/lib/favicon-color-preference";
import { shouldShowFaviconAttentionDot } from "./faviconAttentionDot";
import { AppLayoutSidebar } from "./AppLayoutSidebar";
import { NAV_RAIL_COLLAPSED_SIDEBAR_WIDTH } from "@/components/sidebar/navRailWidth";
import { useSidebarNavigationPlacement } from "@/components/sidebar/sidebarNavigationProvider";
import { SidebarHistoryNavigationControls } from "@/components/sidebar/SidebarHistoryNavigationControls";
import {
useAppCommandHandler,
Expand Down Expand Up @@ -180,15 +181,19 @@ const sidebarOpenAtom = atomWithStorage<boolean>(
);

interface SidebarStateBridgeProps {
framed: boolean;
usesDesktopChrome: boolean;
children: ReactNode;
}

type SidebarResizeMouseEvent = ReactMouseEvent<HTMLDivElement>;
type SidebarOpenChangeHandler = (open: boolean) => void;

function SidebarStateBridge({ framed, children }: SidebarStateBridgeProps) {
function SidebarStateBridge({
usesDesktopChrome,
children,
}: SidebarStateBridgeProps) {
const [open, setOpen] = useAtom(sidebarOpenAtom);
const hasRail = useSidebarNavigationPlacement() === "rail";
const sidebarWidth = useAtomValue(sidebarWidthAtom);
const sidebarLiveWidth = useAtomValue(sidebarLiveWidthAtom);
const handleOpenChange = useCallback<SidebarOpenChangeHandler>(
Expand All @@ -205,8 +210,10 @@ function SidebarStateBridge({ framed, children }: SidebarStateBridgeProps) {
return (
<SidebarProvider
width={`${sidebarLiveWidth ?? sidebarWidth}px`}
collapsedRailWidth={NAV_RAIL_COLLAPSED_SIDEBAR_WIDTH}
framed={framed}
collapsedRailWidth={
hasRail ? NAV_RAIL_COLLAPSED_SIDEBAR_WIDTH : undefined
}
framed={usesDesktopChrome}
data-testid="app-layout-root"
open={open}
onOpenChange={handleOpenChange}
Expand Down Expand Up @@ -822,7 +829,7 @@ export function AppLayout({ children }: AppLayoutProps) {
<ProjectActionsProvider>
<ThreadTitleMentionResourcesProvider {...titleMentionResources}>
<ThreadActionsProvider>
<SidebarStateBridge framed={usesDesktopChrome}>
<SidebarStateBridge usesDesktopChrome={usesDesktopChrome}>
{backToAppRoutePath !== null && !isSidebarResizing ? (
<BackToAppCommandHandler routePath={backToAppRoutePath} />
) : null}
Expand All @@ -840,6 +847,7 @@ export function AppLayout({ children }: AppLayoutProps) {
onResizeMouseDown={handleResizeMouseDown}
isResizing={isSidebarResizing}
settingsRoutePath={settingsRoutePath}
backToAppRoutePath={backToAppRoutePath}
/>
<SidebarInset>
<div
Expand Down
Loading
Loading