Skip to content

Desktop secondary-panel + sidebar UX batch (sawyer-next)#57

Merged
SawyerHood merged 32 commits into
mainfrom
sawyer/ship-2026-05-29
May 30, 2026
Merged

Desktop secondary-panel + sidebar UX batch (sawyer-next)#57
SawyerHood merged 32 commits into
mainfrom
sawyer/ship-2026-05-29

Conversation

@SawyerHood
Copy link
Copy Markdown
Collaborator

Batch of desktop/secondary-panel UX work accumulated on sawyer-next, rebased onto latest main. 31 feature commits + 1 stale-test cleanup.

Highlights

Collapsed conversation rail / panel toggles

  • Collapse the conversation into a slim left rail; panel fills the content area.
  • Rail shows a chat glyph (no chevron); 36px; single clean seam edge.
  • Desktop: rail/panel header no longer collide with the macOS traffic lights (transparent title-bar drag strip + leading reserve).
  • Expand/collapse toggle moved into the panel header (Maximize/Minimize); per-thread collapse state; closed panel shows a PanelRight "open panel" button.

Apps as first-class sidebar rows

  • A manager's installed apps render as rows nested under it; click opens the app expanded with single selection.

Browser surface

  • Minimal, bb-native New Tab screen (no isolated-session pill).
  • Browser tabs no longer pop/reload on tab switch (persistent WebContentsView deck) or flash during resize (live bounds-sync).
  • New desktop setting: open chat http(s) links in the in-app browser.

Secondary-panel tabs

  • Horizontal-scroll overflow with split chevrons flanking the tabs (solid background).

Other

  • New Tab launcher "Recent" section; navigate-to-thread on create; swap a running thread's model/reasoning via CLI; sidebar resize-over-iframe guard; desktop window chrome follows bb's theme.

Migrations

  • 0009_famous_the_hunter (model/reasoning override) sits after main's 0008_thread_pinning; drizzle-kit check clean, migrations apply in order.

Validation

  • typecheck 6/6 packages, lint, @bb/db 294 tests, @bb/app 833 tests — all green on the rebased tip.

Merge style: Rebase and merge (linear history, one commit per change on main).

🤖 Generated with Claude Code

SawyerHood and others added 30 commits May 29, 2026 18:24
The macOS traffic lights + native title-bar chrome tracked the OS
appearance because the window was created without setting
nativeTheme.themeSource (Electron defaults it to "system"). When the
OS was dark but bb was set to light (or vice versa) the window chrome
mismatched the bb UI.

Bridge bb's resolved theme renderer → main over a new
bb-desktop:set-theme IPC channel (validated via a shared zod enum):
a useDesktopThemeSync() hook pushes usePreferredTheme() to
window.bbDesktop.setTheme on mount and on every change (including OS
appearance changes when the preference is "system"); main assigns
nativeTheme.themeSource. The log viewer window inherits it since
themeSource is app-global. No-ops cleanly in the web build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Create App… tile was skipped by the launcher's ArrowUp/ArrowDown +
Enter active-descendant flow because it rendered as a plain button
outside the listbox. Fold it into the navigation model via a
section-layer discriminated union (FileSearchSectionEntry =
{kind:"suggestion"} | {kind:"create-app"}) so the data-layer
suggestion union stays honest; one navigableEntries index space drives
keyboard nav, with Create App appended to the end of the Apps section
(reachable even in the empty state). Extract a shared LauncherTile
shell so the app row and Create App tile share one role/aria/active
contract; the tile now renders role="option" inside the listbox and
aria-activedescendant resolves to its stable id. Enter routes to the
same prefill path as click (confirm + attachment-clear preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Manager thread rows now show a cluster of their installed-app icons at
the trailing edge, left of the branch/environment icon (the status
glyph stays far-right). Click an icon to open that app in the
secondary panel (navigating to the thread if needed). Visible icons
cap at 3 with an informational +N chip whose tooltip names the hidden
apps; empty rows render nothing.

The apps query + open-app hooks live inside ThreadRowAppCluster, which
ThreadRow mounts ONLY for manager rows — so non-manager rows never
instantiate a useThreadApps observer or cache entry (only managers
have apps today). The 30s staleTime is centralized as the useThreadApps
default so the sidebar and detail view share one cache window. Reuses
the secondary panel's open-app path via an extracted
useOpenThreadAppTab(threadId) hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a collapse control (chevron on the thread/panel seam + a mirrored
PanelLeft button in the conversation header) that collapses the
conversation/timeline pane so the secondary panel fills the content
area. Animated via a shared PANEL_COLLAPSE_TRANSITION_CLASS token;
panel lifts to 100% via react-resizable-panels setLayout in a layout
effect (no flicker). State persists in a global
threadConversationCollapsedAtom. Gated to isSecondaryPanelOpen &&
!isCompactViewport — no control when the panel is closed, full no-op
on the compact drawer viewport.

The seam toggle is a higher-stacked sibling of the PanelResizeHandle
(not a child) so react-resizable-panels' overlap-exclusion applies and
clicking it never starts a resize drag — RRP arms the resize from a
capture-phase body pointerdown, which a child guard can't beat. The
collapsed conversation subtree is marked `inert`, removing its
header/timeline/composer from the tab order + a11y tree. Header
control uses aria-expanded to match the seam toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The left-sidebar resize tracked the drag with a window mousemove
listener but had no iframe guard, so an open app iframe in the
secondary panel swallowed the move stream once the cursor crossed it
and the resize froze (most visible with the panel full-width). Mirror
the secondary-panel resize's [&_iframe]:pointer-events-none guard:
while the sidebar is resizing, neutralize iframe pointer events on the
app-layout root. The shared class is extracted into
lib/iframe-drag-guard.ts and used by both the sidebar and
secondary-panel resizes. The guard is removed on every drag-end path
(mouseup, window blur, Escape, effect cleanup) so iframes can't get
stuck non-interactive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the seam-chevron-only collapsed state with the mockup's left
rail: a 48px vertical bar (rotated mono "CONVERSATION" label + an
active-state CircleDashed glyph in the foot) that is fully clickable to
expand. The horizontal PanelGroup stays mounted (timeline → 0%, panel
→ 100% via the existing layout effect) and the rail renders as a flex
sibling in front of it — same [sidebar][48px rail][full-width panel]
visual the mockup shows, without unmounting/reloading the panel iframe
on every toggle. Eases in via the shared transition token. Expanded
rail is w-0 + inert + aria-hidden (no reserved space, no phantom tab
stop); the collapsed conversation stays inert. Expand stays reachable
via the rail, the seam chevron, and the header button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Sawyer's design: drop the header "Collapse conversation" (PanelLeft)
and wide-mode "Show secondary panel" (PanelRight) icon buttons AND the
separate seam chevron, replacing them with a single SeamPanelArrow on
the conversation/panel seam. State→direction:
  - panel closed → ◀ at the content's right edge → "Show panel"
  - panel open, conversation expanded → ◀ → collapse conversation
  - conversation collapsed → ▶ → expand conversation
The 48px rail still expands too. Terminal toggle + ⋯ menu kept; compact
viewport keeps its drawer toggle. The arrow is a higher-stacked sibling
of the resize handle so RRP's capture-phase pointerdown never starts a
resize from it. Reuses threadConversationCollapsedAtom, the visibility
hook, the iframe-drag guard, and the inert collapsed conversation;
ConversationCollapseToggle removed in favor of SeamPanelArrow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a desktop-only web browser as a secondary-panel tab. A new
`browser` tab kind (peer of app/file, per-thread persisted) hosts an
Electron WebContentsView — a real top-level browser context, so sites
that refuse framing (e.g. google.com) load. Chrome: address bar with
URL/search heuristic + https indicator, back/forward/reload/stop,
title in the tab pill, and a new-tab screen (search + quick links +
per-thread recently-visited). The New Tab launcher's OPEN section gains
"Open browser" (gated on window.bbDesktop; web build hides it).

Security: dedicated persist:bb-browser partition; sandbox +
contextIsolation + no node + webSecurity; NO bb bridge injected into
browsed pages; a webRequest firewall blocks loopback/private/LAN hosts
(IPv4 + IPv6, incl. ::ffff: mapped) so untrusted pages can't reach bb's
local services; navigation locked to http(s); popups denied as OS
windows (reopened as in-panel tabs, rate-limited); permissions +
downloads denied; favicons dropped (no attacker URL in the trusted
renderer); URL/title length caps; all IPC zod-validated at both
boundaries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sticky thread-level model + reasoning-level override applied live next turn.
CLI: bb thread update --model/--reasoning-level. Same-provider only; codex gated out for v1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-thread recently-opened panel items with dedup + cap, file-type accents,
relative timestamps, and full keyboard-nav integration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…seam button (#80)

Header top-right chevron opens the panel / collapses the conversation; the 48px
collapsed rail carries an explicit expand chevron. SeamPanelArrow removed; the
label/direction/handler mapping extracted to a shared helper. Also fixes the
rail title/aria-label mismatch and a stale seam comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…psed rail

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e header (#82)

Header toggle renders the PanelRight glyph when the panel is closed (matching
the in-panel hide button) so it reads as 'open the side panel'; chevron retained
for the open/collapse states. iconName moved into resolvePanelToggleControl.
Also fixes a latent rail-label test left by the chat-icon change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…idebar+conversation both collapsed (#81)

Rail expand chevron gets a top inset and the secondary panel top chrome gets a
left inset, gated to macOS desktop + main sidebar collapsed (+ conversation
collapsed for the panel inset), reusing the existing traffic-light reserve
tokens and the AppPageHeader gate signal. Adds parent-gate test coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A manager's installed apps render as rows in the manager's expand/collapse
child region (before worker rows, at the worker indent), opening the app in the
panel on click and collapsing with the manager. Scope-tinted icon tile with a
tile->grip hover crossfade matching the mockup. Removes the duplicate inline
icon cluster (#65). Global/project/agent scopes + drag-to-move deferred.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ns app expanded with single selection (#85)

Removes the scope-tinted tile bg behind the app icon (shared glyph-slot class; dead --scope-* tokens removed), drops the GripVertical drag affordance (icon removed), and makes clicking an app collapse the conversation so the app opens fully expanded with a single selected row (manager row no longer double-highlights).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… tag (#88)

Search field leads (shared Input primitive); removed the globe/heading hero and
the Isolated-session pill; Quick Links + Recently Visited restyled as bb dense
rows with the launcher's relative-time + hover-open affordance. Behavior + browser
security unchanged. New BrowserNewTabScreen test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the secondary-panel resize-handle hairline to zero width while the
conversation is collapsed (it was doubling the rail's recessed edge), and narrow
the collapsed rail from 48px to 36px. Traffic-light insets + resize behavior intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On desktop macOS the rail now renders a transparent h-12 window-drag top strip
(mirroring the sidebar) so its recessed background starts below the traffic-light
strip; the lights sit on clean window chrome. Unifies the old chevron inset into
the strip and removes the now-dead reserve token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scrollable middle tab region (anchored info + new-tab/panel-toggle), reused +
extended OverflowFade for left/right edge fades, hover/focus scroll chevrons on
the overflowing side, active-tab auto-scroll-into-view, and non-passive
wheel-to-horizontal that releases at the edges so page scroll still works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt-row restore (#92)

Conversation-collapse toggle moves to the secondary-panel header (left of
hide-panel) as Maximize2/Minimize2; conversation header keeps only the
closed-state open button; rail unchanged. Conversation-collapsed state is now
per-thread (atomFamily keyed by threadId), and selecting a thread row restores
only that thread's conversation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… trigger when fully collapsed (#93)

When desktop macOS + sidebar + conversation all collapsed, the panel header's
left reserve was pl-16 applied on the same element as px-4 (twMerge replaced it),
landing content at 100px under the pinned sidebar trigger. Use the full pl-20
reserve so leading content sits at 116px — clear of the lights + aligned with
the sidebar-collapse control, matching the page-header pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persistent BrowserTabDeck keeps every open browser tab's native WebContentsView
alive (inactive = display:none, destroyed only on close), with a deck-owned
visibility coordinator that always hides the current view before bounds-syncing
and showing the next — so switching (either direction) never reloads, flashes,
or shows two overlays. Security posture unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the ChevronRight caret from the collapsed rail (the panel-header
Maximize/Minimize is the canonical expand/collapse affordance); recenters the
chat glyph + working indicator. Whole bar still expands on click.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep the native browser view visible during a drag-resize and track its bounds
live (rAF-batched) instead of hiding it; the collapse/expand transition still
hides via isActive. Regression test exercises the real ResizeObserver path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Desktop setting (default on, toggle hidden on web) routes normal http/https
links in assistant chat markdown into the in-app browser surface instead of the
OS browser; mailto/file/relative/internal links unchanged. Reuses openBrowserTab
+ the existing browser security model. onOpenLink is assistant-only (user
messages render as plain text).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In desktop chrome the chevrons picked up MACOS_WINDOW_NO_DRAG_CLASS (relative
z-50), and tailwind-merge dropped their absolute positioning, so both rendered
as flex children before the tabs. Use the pure app-region no-drag class, keep
them absolutely positioned at the scroller's left/right edges. Test asserts the
split placement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The flanking chevrons used the transparent ghost variant, so tab labels bled
through. Add bg-background (the same opaque surface the edge fade resolves to)
so the chevron cleanly occludes the tabs beneath it; ghost hover preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SawyerHood and others added 2 commits May 29, 2026 18:26
Visual polish from live iteration across the secondary panel: browser New Tab
screen, collapsed conversation rail, New Tab launcher recent rows, and the tab
overflow chevrons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bb-native New Tab screen no longer renders quick links (the feature
was removed), but BrowserNewTabScreen.test.tsx still asserted a GitHub
quick-link button, leaving a failing test. Remove it; the component has
no quick-link code remaining.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@SawyerHood SawyerHood merged commit d21fb77 into main May 30, 2026
6 checks passed
@SawyerHood SawyerHood deleted the sawyer/ship-2026-05-29 branch May 30, 2026 01:39
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