Skip to content

Name saved desktop servers, hide This Mac, and switch servers from the palette - #1

Closed
idrevnii wants to merge 21 commits into
mainfrom
bb/desktop-server-manager
Closed

idrevnii wants to merge 21 commits into
mainfrom
bb/desktop-server-manager

Conversation

@idrevnii

Copy link
Copy Markdown
Owner

Human comments

What was wrong

Window → Server names saved servers only by host (10.0.0.3:38886) and always lists This Mac. The rendered app cannot reach that list at all: BbDesktopApi exposes no server targets (get-bb#3133). That works for one extra server. It breaks down for people who deliberately run several independent bb instances, each with its own users, projects, and plugins, and switch between them in one desktop app; for example a personal server, one shared with a friend, and a homelab "ops" server. Those instances are separate on purpose, so moving them onto machines of a single server (the reason get-bb#628 gave for removing the earlier multi-server UI from get-bb#605) is not a substitute. Separately, server-target.json is parsed with a strict schema: a file containing any field the running build does not know loads as "no saved servers", and the next save (for example Add Server…) overwrites it. Moving between desktop builds, in either direction, can therefore silently erase the saved list.

What changed

This keeps server targeting in the native menu, the direction set by get-bb#628 and get-bb#3919, and adds no renderer management UI.

Desktop shell (apps/desktop):

  • server-url-dialog*.ts: the existing Add Server… and Set Server URL… dialogs get an optional Name field, using the same markup and CSS as the URL field.
    • The name replaces the host in the menu.
    • Adding an already-saved URL with a name renames it; with no name, the saved name is kept.
  • server-target.ts:
    • Set Server URL… now edits the server in place, keeping its position and name instead of moving it to the end. Clearing the URL still removes the server.
    • Names (keyed by URL) and the new This Mac choice are stored in a separate server-menu.json. server-target.json keeps exactly the format released builds write, so older and newer builds read each other's files.
    • Unknown fields are now ignored instead of invalidating the file. A missing or corrupt server-menu.json only resets names and the This Mac choice.
  • menu.ts, server-list.ts:
    • A Show This Mac checkbox at the bottom of the Server menu.
    • This Mac stays listed while it is the current server or when no other server is saved, so hiding it can never leave the menu empty.
  • preload.ts, packages/desktop-contract/src/servers.ts: optional getServerTargets, onServerTargetsChange, and selectServerTarget members on BbDesktopApi, as Expose the Desktop server selector in the sidebar header get-bb/bb#3133 proposes.
    • They carry names and opaque ids (a hash of the URL), never URLs.
    • Selection is accepted only from application-window main frames, must match a listed id, and runs through the same setActiveServerTarget path as the menu.

App (apps/app):

  • The quick palette offers Switch to server: <name> for every other listed server (palette-server-actions.ts, useDesktopServerTargets.ts).

Other surfaces:

  • Docs: apps/desktop/README.md (Saved servers) and the verify-bb desktop recipe.
  • Wire and APIs: no server/daemon wire change, so no HOST_DAEMON_PROTOCOL_VERSION bump. No plugin API change.
  • CLI: no CLI command. The saved-server list and menu are local to the desktop shell (as with dataDirectory.open in Add a quick palette command to open the data directory get-bb/bb#4256), and the CLI already targets any server through BB_SERVER_URL.

Deviation from get-bb#3133: it asks for a sidebar selector. This PR adds the bridge it describes but switches from the quick palette only, keeping app chrome unchanged; a sidebar control could build on the same members. Related: get-bb#1451 (multiple desktop servers). The This Mac checkbox also covers the "hide This Mac from the Server menu" part of get-bb#3168, though not its request to stop starting the bundled runtime.

Palette entries need both a desktop build and a served app that include this change; older desktops simply omit the optional bridge members.

How you verified

  • pnpm exec turbo run typecheck lint --filter=@bb/app --filter=@bb/desktop --filter=@bb/desktop-contract
  • pnpm exec turbo run test --filter=@bb/desktop --filter=@bb/desktop-contract: 444 + 22 passed, 1 skipped (existing).
  • pnpm exec turbo run test --filter=@bb/app: 5076 passed, 5 skipped (existing).
  • The data loss described above was confirmed against main's server-target.ts with a real file:
    • a file with two saved servers plus one unknown field loaded as [] / builtin;
    • after Add Server… the file held only the new server.
  • New and updated tests:
    • server-target.test.ts:
      • naming;
      • keeping a name across reselection;
      • in-place URL edits and clearing a name;
      • rejecting an over-long name without writing either file;
      • reload persistence;
      • the written server-target.json passes a copy of the released strict schema;
      • a file with unknown fields (including one written by an earlier build of this branch) keeps all saved servers and the target;
      • a corrupt server-menu.json leaves the server list intact.
    • server-list.test.ts: names and hosts, This Mac visibility rules, opaque ids.
    • menu.test.ts: the Show This Mac checkbox.
    • palette-server-actions.test.ts and CommandPalette.test.tsx: switching from the palette.
  • Manual check on Linux: source-built Electron with an isolated profile against pnpm start:worktree, with two saved servers pointing at it through 127.0.0.1 and localhost, driven through CDP.
    • Set Server URL opened with the URL and name filled in, and renaming updated the menu.
    • Add Server with an existing URL and a name renamed that server and switched to it.
    • Unchecking Show This Mac removed it from the menu and the bridge.
    • The palette listed the other server, and selecting it loaded that server.
  • Not verified: physical macOS; menu behavior there is unchanged Electron menu code.

Refs get-bb#3133, get-bb#1451, get-bb#3168

🤖 Generated with Claude Code

AGENT GENERATED

SawyerHood and others added 21 commits September 24, 2026 09:51
…4268)

## Human comments

## What was wrong

When the gate's tunnel Durable Objects restart, hibernated tunnel
WebSockets can be orphaned. The bb keeps an open socket that nothing
answers, so its heartbeats go unanswered. The object has no tunnel
socket, so visitors get `503 offline`. After 60 s the bb redials, and
while the object keeps getting requests, every new dial is orphaned the
same way. An object heals only after about 45 s with no requests, which
busy servers never get. get-bb#4251 reproduced this on staging: any deployment
of `bb-connect`, even an unchanged version, triggers it (runs X0–X7
there).

This caused the 2026-09-24 01:07 outage, and it's happening again in
prod **without any gate deploy**. Prod `bb-connect` has stayed on
`6f79c289` since the 01:43 rollback. Since about 07:00 UTC, Durable
Object outcomes show recurring bursts at 08:45–10:30, 12:30, 13:30,
14:30 and 16:15, each with `responseStreamDisconnected` up 5–10× and
`scriptThrewException` up 10–20×. Each burst leaves more servers stuck:

- the 503 share on `*.getbb.app` climbed from 9–10% overnight to 16–30%
per hour;
- 112 hosts had 100 or more extra 503s from 14:30 to 16:30;
- bee's log shows `tunnel heartbeat missed` every minute again.

The current gate has no way to recover.

## What changed

This is the `TunnelDO` self-restart from get-bb#4251, split out so it can ship
ahead of the account stack. It contains no ticket code and no workflow
changes.

- `apps/connect/src/tunnel-do.ts`:
- It records `tunnelOpenedAt` when it accepts a tunnel and
`tunnelClosedAt` when the last tunnel socket closes or the owner revokes
it.
- When a visitor request, a tunnel dial, or the 50 s presence alarm
finds no live tunnel socket although the last one never closed (after a
5 s grace), it writes the close, calls `storage.sync()`, then
`ctx.abort()`. The restarted object drops the orphaned socket and the bb
redials into a working object.
- `sync()` is required. Without it, `abort()` discards the write and the
object restarts forever (586 aborts in staging run B1 of get-bb#4251).
- Objects written by older code only have `serverId`/`machineId`; they
restart once the same way, so the first deploy of this code heals
existing stuck objects.
  - The alarm path covers servers nobody is visiting.
- `apps/connect/src/worker.test.ts`: a `TunnelDO restarts after its
tunnel socket vanishes` suite. It restarts on a visitor request, a dial
and the alarm. It doesn't restart after a recorded close, within the
grace period, or with a live socket. Legacy objects restart exactly
once, and the close record survives the restart.

## How you verified

- On `origin/main`'s `tunnel-do.ts` the new suite fails 5 of 9; with
this change 9 of 9 pass.
- `pnpm exec turbo run typecheck lint test --filter=@bb/connect`: pass
(130 tests).
- Staging evidence from get-bb#4251, using the same `tunnel-do.ts` logic:
- B2: the first deploy of this code over the old gate brought every
tunnel back within about 40 s, and DO `internalError` went 205 → 0.
  - B3 and X10b: later deploys brought each tunnel back within 5–10 s.
- Upgrade step B: a pre-stack bb held one tunnel for 30 minutes with 0
heartbeat misses, and visitors got 347/347 200s.

## Rollout

**Deploy Connect is enabled, so merging deploys this with `wrangler
deploy` at 100%.** That's acceptable: the deploy orphans the tunnels,
and this code restarts them. On staging (get-bb#4251 B2), all tunnels were
back within about 40 s. The gentler path, recommended in get-bb#4251, is:

1. Disable Deploy Connect.
2. Merge.
3. `pnpm --filter @bb/connect exec wrangler versions upload`.
4. `pnpm --filter @bb/connect exec wrangler rollback <new-version-id> -m
"gate: tunnel self-restart"`. A rollback to a never-deployed version
closed tunnels cleanly on staging (get-bb#4251 X11).
5. Re-enable Deploy Connect.

Either way, go straight to 100%: objects still on old code can't
restart.

For 30 minutes afterwards, watch:

- the 503 share and 503s per host on `*.getbb.app`;
- Durable Object `internalError` (one spike as stuck objects restart,
then 0) and `clientDisconnected`;
- `grep 'plugin:connect\] tunnel' ~/.bb/logs/server-stdio.log` on bee.

## Rollback

`pnpm --filter @bb/connect exec wrangler rollback
6f79c289-9e43-4697-b056-78e31bc3e6ad -m "<reason>"`. Use `wrangler
rollback`, never `versions deploy <old>@100%`: rollbacks closed tunnels
cleanly every time we saw one, and a deployment orphans them again.

> AGENT GENERATED

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

Appearance placed sidebar provider choices ahead of Theme and Palette
and mixed interface controls with appearance choices in one card. The
order did not reflect how people use the page, and Sidebar footer
customization sat among the appearance controls.

## What changed

- Put Theme and Palette first in Appearance, followed by Favicon color
and Fade inactive splits.
- Add an Interface card containing Header, Source code, Diffs, Sidebar,
Navigation, and Sidebar footer in that order. The first three selectors
appear when a plugin registers them.
- Add a focused layout test with all optional selectors registered. No
wire protocol, CLI, SDK, or configuration contract changed.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --
src/views/SettingsView.appearance-layout.test.tsx
src/views/SettingsView.palette-preview.test.tsx
src/components/plugin/PluginSidebarFooterItems.test.tsx` — 17 passed.
- `pnpm exec turbo run lint typecheck --filter=@bb/app` — passed; lint
reported 0 errors and 200 warnings outside this change.
- `pnpm start:worktree --dryrun` — passed, then started the production
worktree app at commit `05d063e736018f9f2bac4e2200a72359f84341cc`.
- Installed a temporary local fixture in the isolated preview store. In
the real app, confirmed Header, Source code, and Diffs appear in
Interface with Sidebar, Navigation, and Sidebar footer.
- Live review route (owner's BB Connect session required):
https://ymichael--21547.getbb.app/settings/appearance

Final GitHub checks for `05d063e736018f9f2bac4e2200a72359f84341cc`: 16
passed, 2 skipped, 0 failed.

No related issue was found.

> AGENT GENERATED
## Human comments

## What was wrong

The desktop app's Server switcher (saved servers, Add Server…, Set
Server URL…) was only in **Window → Server**. That's an odd place for
app-level configuration, and people looking in the bb app menu next to
Settings didn't find it.

## What changed

- `apps/desktop/src/menu.ts`: the bb app menu gets a **Desktop
Settings** submenu (after Settings…) containing the **Server** submenu.
**Window → Server** stays as an alias built from the same
`createServerMenuItems(args)`. Each copy has its own menu item ID
(`bb-desktop-settings-server-menu`, `bb-server-menu`), and
`installApplicationMenu` attaches the `menu-will-show` refresh hook to
both, so the server list stays current whichever copy is opened.
- `apps/desktop/test/menu.test.ts`: a new test checks that Desktop
Settings → Server shows the same items as Window → Server, and that
selecting a server and Add Server… from the new location call the right
handlers.
- `apps/desktop/README.md`: the Saved servers section now points to the
new location and mentions the Window alias.

No wire, CLI, or protocol changes.

## How you verified

- `pnpm exec turbo run test typecheck --filter=@bb/desktop`: typecheck
passes and 435 tests pass. In that full parallel run
`test/preload-build.test.ts` failed once. It passes on its own (`vitest
run test/preload-build.test.ts test/menu.test.ts`: 9/9) and doesn't
touch the menu code.
- Manual check of the packaged desktop app on macOS still to come.

> AGENT GENERATED

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

The Interface card introduced in get-bb#4241 renders its rows in source order.
Header and the code renderer controls were placed before Sidebar and
Navigation, so the visible order differed from the requested sequence.

## What changed

- Render Sidebar and Navigation before Header, Source code, and Diffs;
Sidebar footer remains last.
- Delete the dedicated Appearance layout test as requested. No wire,
SDK, CLI, or configuration contract changed.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --
src/views/SettingsView.palette-preview.test.tsx
src/components/plugin/PluginSidebarFooterItems.test.tsx` — 16 passed.
- `pnpm exec turbo run lint typecheck --filter=@bb/app` — passed; lint
had zero errors and 201 existing warnings.
- `pnpm start:worktree --dryrun` — passed, then started the production
worktree app at `504ef46d0befd05729a95cb2d6125baba32225ab`.
- In the real app with a temporary plugin fixture, verified Sidebar →
Navigation → Header → Source code → Diffs → Sidebar footer on Settings →
Appearance.
- [Live review
page](https://ymichael--21547.getbb.app/settings/appearance) requires
the owner's BB Connect login.

> AGENT GENERATED
…ancelled (get-bb#4187)

## Human comments

## What was wrong

Cancelling a thread's environment preparation retired whatever
environment the thread held immediately, bypassing the provider's
`retireGraceMs` policy. When a new thread starts on a project checkout
that already has an environment, the checkout provider returns the
existing path and `bindEnvironmentPath` merges the thread's reservation
into the existing environment row, so the thread now holds a ready,
shared environment. The placement pass then compared that row's stored
selection (the original thread's `branch: new` input) with the new
request, saw a difference, and cancelled. The cancel retired the shared
checkout environment at once even though the project checkout provider
declares `retireGraceMs: null`, and the new thread failed with
`Environment is destroyed`. Any other cancel of such a thread (archive,
stop, deletion, restart during provisioning) followed the same path.

## What changed

- `apps/server/src/services/environments/environment-engine.ts`: when
the sweep sees a cancelled environment that is already `ready` with a
workspace path, it releases the thread's hold on it and applies the
provider's normal retirement policy instead of removing it immediately.
Unfinished environments (`creating`, `provisioning`, `error`) are still
removed immediately, as before. Project checkouts (`retireGraceMs:
null`) are therefore never removed by a cancel; a cancelled but finished
worktree now waits out its grace period (default 5 minutes), as it would
after archiving a thread.
- `apps/server/src/services/threads/thread-environment-placement.ts`:
the selection comparison that triggers a cancel is skipped once the held
environment is `ready` (provider and host changes still trigger it), so
a stored selection from the environment's original creator no longer
counts as a changed request.
- `plugins/environment-project-checkout/server.ts`: implements
`experimental_existingPath` for branchless selections that name a path,
so such spawns (the reporter's `--environment <path>`) reuse the
recorded environment directly instead of creating a reservation and
merging it.

No wire, CLI, SDK, or schema changes. This deviates from the issue's
suggested fixes: the stored selection is kept on the shared environment
because it is used to rebuild the environment later.

## How you verified

- New server tests in `provider-orchestration.test.ts`: the reporter's
regression (adopted checkout with a previous selection), cancel after
takeover under both `retireGraceMs: null` and a grace period, cancel
while creation finishes, and retirement of a thread's own cancelled
ready environment after its grace. Each fails with its source change
reverted.
- New plugin test for when the checkout provider reuses an existing
path.
- `pnpm exec turbo run test typecheck lint --filter='...[origin/main]'`:
server, plugin, and dependents pass. Two server tests and six
integration files timed out under parallel load; all pass when rerun in
isolation (22/22 and 15/15).
- Live, in isolated `pnpm start:worktree` instances of `origin/main`
(4277998) and this branch, with identical scripted fixtures on
scratch repos: after a `branch: new` thread is archived, a second thread
on the same checkout fails with `Environment is destroyed` and the
environment is `destroyed` on main (via both the composer-style no-input
spawn and `--environment <path>`), while on this branch the second
thread starts and the environment stays `ready`. A second thread sharing
a checkout with a live thread starts normally on both.

Fixes get-bb#4186

> AGENT GENERATED
<!-- slopcop:rule=new-issue-autopilot run=run_8i9t9arxmueclxz1
sha=issue-4186 kind=summary -->

---------

Co-authored-by: SawyerHood <sawyerjhood@gmail.com>
Co-authored-by: Michael Yong <wrong92@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ng installed plugins (get-bb#4258)

## Human comments

## What was wrong

Before bb's thread list and navigation became bundled plugins,
`sidebar.threadListProvider` and `sidebar.navigationProvider` defaulted
to `__automatic__`, which showed the first installed plugin for the slot
(`resolveReplacement` → `registrations[0]`). Once the bundled
`thread-list` and `navigation` plugins registered in the same slots,
Automatic became a plugin-id ordering contest (`bb-sidebar` sorts before
`thread-list` and wins, `zen-list` would lose). get-bb#4051 (shipped in
0.43.4) and get-bb#4118 (unreleased) resolved that by making the bundled
plugins the defaults and reading `__automatic__` as them. Installations
that relied on Automatic had no stored choice, so after upgrading they
silently lost their community thread list (get-bb#4150, e.g. BB Sidebar), and
installing a thread list or navigation plugin no longer had any visible
effect. Navigation users (e.g. Compact Nav) would hit the same switch in
the next release.

## What changed

Automatic is the default again for both keys, redefined as "the first
registered provider other than the bundled plugin, falling back to the
bundled plugin":

- `packages/domain/src/ui-preferences.ts`: both keys default to
`__automatic__` and keep it; legacy `__builtin__` still resolves to the
bundled plugin, and explicit choices are preserved. Reset returns to
Automatic.
- `apps/app/src/lib/plugin-replacement-preference.ts`:
`resolvePreferredReplacement` takes an optional bundled provider key
that Automatic skips unless it is the only registration
(`findAutomaticReplacement`). Other exclusive slots (code/diff
renderers, file openers) pass no key and behave as before.
- `threadListProvider.ts` / `sidebarNavigationProvider.ts`: pass
`thread-list/thread-list` and `navigation/navigation`.
- Settings → Appearance: the Sidebar and Navigation pickers offer
Automatic again and name the provider it currently resolves to. The
header picker is unchanged (new slot, never shipped an Automatic
default).
- `SidebarNavigationRegion.tsx`: the loading skeleton records its height
under the selected preference, which is what it already read from, so
the Automatic default keeps its remembered height.
- `packages/plugin-sdk/src/app-contract.ts`: the
`experimental_threadList` and `experimental_sidebarNavigation` doc
comments now describe the Automatic default, pinning, and placeholders
(they described bundled-plugin defaults, a removed `Original` prop, and
a fallback to bb's list). Plugin SDK bumped to 0.5.25 for the published
doc change.
- Docs: configuration, bb guide template, bb-cli skill reference,
plugin-authoring reference, Plugin Guide surface,
`docs/plugin-sidebar-thread-list.md` (which still described client-local
`localStorage` selection), and the API audit entries.

Deviation from the issue's proposed fixes: no migration or notice.
Restoring Automatic with the bundled plugin as the fallback covers
upgraded installs (an unset preference is Automatic again) and the
issue's new-install case (installing a sidebar plugin takes effect),
without per-install state. This reverses the opt-in default from
get-bb#4051/get-bb#4118; the thread-list plan (`plans/thread-list-plugin.md`)
originally intended to keep Automatic-first. No server/daemon wire
change.

## How you verified

- New tests fail under the old first-registration rule and pass now
(checked by swapping in the old resolver): resolver prefers an installed
replacement over the bundled one regardless of plugin-id order
(`plugin-replacement-preference.test.ts`), thread list setting defaults
to Automatic and names the installed plugin, navigation region renders
an installed navigation plugin over the bundled one by default. Plugin
ids deliberately sort after `thread-list`/`navigation`.
- Updated server preference tests: `__automatic__` and explicit values
kept, `__builtin__` → bundled, reset → `__automatic__`.
- `pnpm exec turbo run typecheck lint --filter='...[origin/main]'` (123
tasks pass).
- `pnpm exec turbo run test --filter=@bb/app --
plugin-replacement-preference SidebarThreadListSetting
SidebarNavigationSetting SidebarNavigationRegion PluginThreadList
SidebarHeader`, `--filter=@bb/server -- public-ui-preferences`,
`--filter=@bb/domain`, `--filter=bb-plugin-plugin-api-docs`,
`--filter=@bb/cli` (one unrelated `server-export` test failed once in
the full run and passed on rerun).
- Live QA with a local community fixture plugin (`zen-sidebar`,
registering a thread list and a navigation provider; id sorts after
`navigation`/`thread-list`), driven through the UI with headless
Chromium and the source CLI:
- PR HEAD (`pnpm start:worktree`, fresh data): unset preferences read
`__automatic__`; after installing the fixture the sidebar renders Zen's
list and navigation; Settings → Appearance shows "Automatic — Currently
using Zen Sidebar from zen-sidebar" / "Zen Nav". Picking Thread list and
Navigation in the pickers stores revision 1 explicit values, and they
stay bundled after reload and after disabling/re-enabling the fixture.
`bb settings ui reset` returns both keys to `__automatic__`.
- Control at the merge base (same fixture, same steps): the fixture is
enabled and running but the bundled list and navigation render; the
pickers have no Automatic option; `bb settings ui set
sidebar.threadListProvider __automatic__` is stored as
`thread-list/thread-list`.
- Published packages: `bb-app@0.43.3` with the fixture (thread-list-only
variant, because 0.43.3's SDK lacks `experimental_useSidebarNavigation`)
shows Zen; upgrading the same data dir to `bb-app@0.43.4` shows the
bundled list while Zen stays enabled/running and `ui_preferences` has no
row. This branch's `bb-app` build on a copy of that upgraded data dir
shows Zen again.
- Startup flash caveat: with the community bundle artificially delayed
2.5 s, Automatic shows the bundled list/navigation until the community
plugin registers (~0.27 s → 2.6 s), then switches; a pinned community
provider shows the loading placeholder instead. Without the delay no
bundled frame was observed (40 ms sampling). 0.43.3's Automatic had the
same pattern (built-in list for ~50 ms before the plugin).

Fixes get-bb#4150

> AGENT GENERATED

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

The project checkout picker disabled New branch and Checkout when Git
state blocked branch changes, but exposed the reason only in a title
tooltip. That left the choices unexplained for touch, keyboard, and many
pointer users.

## What changed

The picker now displays a short, actionable explanation beneath the
disabled choices for a dirty checkout, conflicts, an in-progress
operation, detached HEAD, an empty repository, and unavailable checkout
state. The existing server and host safeguards remain in place. No wire
contract or CLI behavior changed.

## How you verified

- `pnpm exec turbo run typecheck
--filter=bb-plugin-environment-project-checkout`
- `pnpm exec turbo run test
--filter=bb-plugin-environment-project-checkout` (39 tests passed)
- Opened the picker in a running source app with a synthetic dirty
checkout and confirmed both options are disabled while the commit or
stash guidance is visible.

> AGENT GENERATED
## Human comments

## What was wrong

The nonexpandable system-row header aligned its leading icon and title
by baseline. This raised the icon relative to the title for “Stopped —
connection to host was lost” and other short system operation rows. See
get-bb#4282.

## What changed

Center the icon and title in the shared static row wrapper, consistent
with expandable timeline rows. Add an `IconAlignment` Ladle story with
the reported title and adjacent compaction, context-clear, and
parent-change variants.

## How you verified

- Captured original and corrected rendering in the same Chromium
viewport through the direct Ladle story.
- Opened a disposable thread containing the host-loss event in a `pnpm
start:worktree` production build at this PR head; the row renders after
reload.
- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/thread/timeline/ThreadTimelineRows.system-summary.test.tsx`
(8 passed).
- `pnpm exec turbo run typecheck --filter=@bb/app` (passed).
- `pnpm start:worktree --dryrun` (48 build tasks succeeded); the built
server and daemon health endpoints pass.

Fixes get-bb#4282

> AGENT GENERATED
…-bb#4276)

## Human comments

## What was wrong

Host plugin builds resolved relative imports twice: the private-package
validator called `build.resolve` for each source dependency edge, then
esbuild resolved the same graph for bundling. These extra resolver round
trips dominated preparation time even for a small plugin such as Keep
Awake.

## What changed

Validate package ownership against esbuild's actual input graph,
retaining source checks for direct private BB imports. Relative and
absolute imports erased from that graph, including type-only and
dead-code imports, receive a separate validation-only pass. Failed
validation preserves previously built artifacts and cleans up staging
files.

This PR contains only commit `5fa41fbffa`; subsequent CSS and scanner
optimizations are excluded.

## How you verified

- Ran builder build, typecheck, and tests through Turbo: 160 tests
passed, one skipped.
- Added coverage for erased imports, re-exports, dynamic imports,
require calls, JSON, symlinks, aliases, transitive private imports,
unresolved type-only imports, and output preservation.
- Eight alternating fresh-process Keep Awake builds: median preparation
time fell from 330.1 ms to 203.5 ms (38.4%).
- Full startup comparisons with empty Turbo caches, warmed toolchains,
and unchanged concurrency: 8.62 → 6.77 seconds and 7.89 → 6.79 seconds.
The first baseline also had a slower app build; the second pair supports
roughly 1.1 seconds saved overall.

> AGENT GENERATED
…b#4270)

## Human comments

## What was wrong

Claude Code emits one SDK `assistant` message per content block, so when
a single API response contains more than one thinking block, each
finalized block arrives as its own message with the block at local index
0. The translator keyed reasoning streams by content index: stream
deltas used the API index (`thinking-1` for the second block) but the
finalizing `item.textClose` used the local index (`thinking-0`). The
second block's streamed item never received a close, so it stayed in
progress forever, and a duplicate completed reasoning item was created
alongside it.

This depends on the model, not on the CLI or SDK version. Across local
transcripts, `claude-opus-5` produced multiple thinking blocks in ~0% of
responses, while `claude-fable-5`/`fable-5-1` did in 2–21% and
`claude-opus-5-5` in ~13% (always `thinking > thinking > tool_use`).
Almost every never-completed Claude reasoning item in a local `bb.db`
since mid-August follows this mis-keyed pattern, with a sharp jump after
moving to Opus 5.5 (70 on 2026-09-23).

## What changed

- `plugins/provider-claude-code/src/delta-translation.ts`: thinking
streams and finalizations share a single `thinking` key, matching how
assistant text is already keyed. Blocks are finalized in order before
the next one streams, so each block opens and closes its own item.
- `plugins/provider-claude-code/src/sdk-extraction.ts`: drop the
now-unused `contentIndex` from the stream/thinking extraction helpers;
they return plain strings.

No wire change: the stream key lives only in the in-memory
translator/assembler state, and emitted events keep the same shape. No
`HOST_DAEMON_PROTOCOL_VERSION` or Plugin SDK change. Already-orphaned
reasoning items in existing threads are not backfilled.

## How you verified

- New test `finalizes each thinking block of a response on its own
assistant message` streams two thinking blocks (index 0 and 1), each
finalized by a single-block assistant message, and asserts
start/complete pairs with matching IDs. It fails before the fix and
passes after.
- `pnpm exec turbo run test typecheck lint
--filter=bb-plugin-provider-claude-code`: 366 tests pass, typecheck and
lint clean.

> AGENT GENERATED

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
)

## Human comments

## What was wrong

On phones, the sidebar collapse caret on a parent thread row is a 20×20
px button, and since get-bb#4077 and get-bb#4098 every other point of the row opens
the thread. get-bb#4077 widened the parent row's navigation link to fill the
title area around the caret, and get-bb#4098 forwarded taps on empty row space
and the status area to that link. So a tap that just misses the caret
now opens the thread and closes the compact sidebar. Other row controls
grow to 36×36 px on touch screens; the caret never did. get-bb#3195 reported
the same 20×20 target for this control, but its fix (get-bb#3199) only
enlarged the caret in mobile Recents.

## What changed

`plugins/thread-list/app/rows/SidebarChildToggleChevron.tsx` gives the
caret an invisible `::after` hit area on coarse-pointer viewports below
`md`: 36×36 px, extending 8 px up and down, 6 px left (exactly the gap
to the title, so title taps still navigate), and 10 px right. The
button, icon, and hover/pressed background keep their 20 px size, so
nothing looks different. Desktop and fine-pointer layouts are unchanged.
The shared component also serves environment-group rows and nested
section rows. On environment rows, the neighbouring action buttons come
later in the DOM at the same z-index, so they still win their own taps.
No wire, CLI, SDK, or config changes.

## How you verified

- `pnpm exec turbo run typecheck lint --filter=bb-plugin-thread-list`:
pass (0 errors; 7 existing warnings).
- Production `pnpm start:worktree` at this head, with before measured on
the same build by hiding only the new `::after`, which is the entire CSS
change.
- **iOS Safari 26.5, iPhone 17 Pro Max simulator** (safaridriver, W3C
touch actions, 3 trials per point):
- Parent thread row: taps 4 px left, 6 px right, and 6 px below the
caret opened the thread 3/3 before; now they collapse 3/3. Caret center
collapses 3/3 both ways. Title center and trailing status still open the
thread 3/3.
- Environment-group row: caret and near-miss taps collapse 3/3 both
before and after. "New thread in environment" and "Environment actions",
at their centers and 2 px inside their left edges, still navigate or
open their menu 3/3.
- **Chrome 390×844 with touch emulation** (CDP touch events, 10 px
radius): the same matrix gave the same outcomes. `elementFromPoint`
confirms the hit area is 36×36 and starts at the title's right edge.
- Screenshots of the sidebar with and without the hit area are
pixel-identical. At 1280 px the `::after` is not generated.

Related: get-bb#3195, get-bb#4077, get-bb#4098, get-bb#3989.

> AGENT GENERATED

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

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

The shared Settings provider menu listed registered slots in
registration order. Bundled Navigation and Thread list slots looked like
third-party entries, so the community choices could appear below them
and their descriptions gave no source cue. Automatic already prefers a
third-party slot when one is installed (related: get-bb#4258).

## What changed

Order menu entries as Automatic, third-party slots, bundled slots, then
the owner-built-in option where available. Label bundled slots as
built-in BB defaults, identify third-party plugin descriptions, and
describe what Automatic chooses. Provider resolution, saved keys, plugin
APIs, CLI, and server/daemon wire contracts are unchanged.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/settings/SidebarNavigationSetting.test.tsx
src/components/settings/SidebarThreadListSetting.test.tsx
src/components/settings/CodeRendererSettings.test.tsx` (5 passed)
- `pnpm exec turbo run typecheck --filter=@bb/app` (passed)
- `git diff --check` (passed)

> AGENT GENERATED
## Human comments

## What was wrong

`matthiasvongrundherr` was absent from the approved contributors list,
so the PR gate would close their pull requests.

## What changed

Added `matthiasvongrundherr` to `.github/APPROVED_CONTRIBUTORS`.
Approval takes effect when this change lands on the base branch.

## How you verified

Verified the GitHub login with `gh api`, confirmed the login appears
exactly once, reviewed the single-line diff, and ran `git diff --check`.
CI is intentionally skipped for this data-only change.

> AGENT GENERATED
…eted event (get-bb#4244)

## Human comments

## What was wrong

The connect plugin persists shared ports per host, but nothing ever
pruned shares whose host was destroyed. Core never told plugins a host
was removed, so such shares stayed forever unless someone ran `bb
connect unexpose` by hand. This keeps happening with Modal sandboxes,
which are created and destroyed often.

It was also worse than clutter: `ShareRegistry.declareMachineShares`
re-declares ports for every machine host on each tunnel share
activation, and the server's `declareSharedPorts` throws `cannot declare
shared ports for unknown host <id>` for a destroyed host. That error
failed the whole activation, so `TunnelManager.activateShares`
rescheduled itself every 5s indefinitely and never reached
`shares.list()` / `publish()` for the remaining valid shares. On a real
instance with one orphaned Modal-sandbox share, this logged two warnings
every 5s (230 pairs in 20 minutes).

## What changed

**New plugin event `experimental_host.deleted`**
(`packages/plugin-sdk/src/backend-contract.ts`)
- Delivers `{ host }` once after a machine is removed. It fires from
`DELETE /hosts/:id` for manually added machines
(`apps/server/src/routes/hosts.ts`), and after a machine provider
finishes removal
(`apps/server/src/services/machines/provider-orchestration.ts`). Those
are the only two places that set `destroyedAt`.
- `host` is the public `Host` DTO at removal time, with status
`disconnected`, because `bb.sdk.hosts.get` returns 404 for the host
afterwards.
- Plumbed through the existing thread-event emitter, like
`experimental_terminal.input`: `plugin-thread-events.ts`,
`PluginThreadEventEmitter`, the plugin-service implementation, and the
handler maps in `plugin-api.ts` and the fake plugin host.
- `@bb/db` now exports `HostRow`, replacing a local alias in
`entity-lookup.ts`, and `toHostRecord` is exported so the plugin service
can build the DTO.

**Connect prunes removed hosts' shares**
(`plugins/connect/src/shares.ts`, `server.ts`)
- New `ShareRegistry.pruneHost(hostId)` removes every share for that
host, clears its declaration bookkeeping and snapshot entries, persists
the change, and publishes the updated status. If persistence fails, it
restores the removed in-memory shares so the next activation can retry
pruning.
- Pruning happens on `experimental_host.deleted`.
- It also happens at activation when declaring a host fails and
`hosts.get` confirms the host is gone (404). This covers hosts removed
while the plugin wasn't loaded or before this change. Declaration
failures on hosts that still exist are still logged and rethrown, so
retry behaviour for transient errors is unchanged. Merely disconnected
hosts are never affected, since the server only rejects declarations for
missing or destroyed hosts.

**Docs and version**
- Plugin SDK bumped 0.5.24 → 0.5.26; main has since reached 0.5.25, so
0.5.26 keeps the new event on a distinct SDK version.
- New `docs/api_to_audit.md` entry with stabilization criteria.
- Plugin Guide surface bullet in
`plugins/plugin-api-docs/src/surfaces.ts`.
- `backend-events.md` authoring reference.
- Payload-field map in `plugin-authoring-docs.test.ts`.

No daemon wire changes.

## How you verified

Each new test fails without its piece of the fix and passes with it:
- `public-host-management.test.ts`: `DELETE /hosts/:id` announces the
destroyed host row once.
- `provider-orchestration.test.ts`: provider machine removal announces
it once, even when a second sweep runs.
- `connect.test.ts`: `experimental_host.deleted` prunes all of that
machine's shares and keeps others, both in storage and in `listShares`.
- `connect.test.ts`: activation prunes a removed host's share without
logging, and still rejects on a live host's declaration failure.
- `connect.test.ts`: a failed KV write leaves the removed host's share
retryable; the next activation prunes it when storage recovers.

Commands run:
- `node .github/workflows/check-plugin-sdk-version.mjs`: passes with the
0.5.26 bump.
- `pnpm exec turbo run test typecheck --filter=@get-bb/plugin-sdk
--filter=bb-plugin-connect`: 358 SDK and 112 Connect tests pass; both
typechecks pass.
- `pnpm exec turbo run test --filter=@bb/db`: 593 database tests pass.
- GitHub CI on the merged branch: all checks pass. The packages job
passed on its third attempt after two attempts hit five-second timeouts
in different database migration tests under full CI load.
- `pnpm exec turbo run test typecheck --filter=bb-plugin-connect`: 112
tests pass; typecheck passes after the retry fix.
- `pnpm exec turbo run typecheck lint --filter='...[origin/main]'`:
124/124 tasks pass.
- `pnpm exec turbo run test --filter='...[origin/main]'
--filter='!@bb/desktop' --continue`: 82/84 tasks pass.
- Three tests failed under full parallel load and all pass when re-run
in isolation: `install-machine-script` and `builtin-plugins` hot-reload
in server, `CommandPalette` in app.
- `@bb/desktop` was excluded because this worktree's Electron install is
broken ("Electron failed to install correctly"). That is unrelated to
this change.
- `node .github/workflows/check-plugin-sdk-version.mjs`: "surface
changed … with a version bump".

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
## Human comments

## What was wrong

`idrevnii` was absent from the contributor allowlist, so the PR gate
would close their pull requests.

## What changed

Added `idrevnii` to `.github/APPROVED_CONTRIBUTORS`.

## How you verified

Confirmed the GitHub login, verified it appears exactly once, reviewed
the one-line diff, and ran `git diff --check`. CI is skipped for this
data-only change.

> AGENT GENERATED
## Human comments

## What was wrong

`trotterdylan` was absent from the approved contributor list, so the PR
gate would close their pull requests.

## What changed

Added `trotterdylan` to `.github/APPROVED_CONTRIBUTORS`.

## How you verified

Verified the GitHub account through the GitHub API, confirmed the login
appears exactly once, reviewed the single-line diff, and ran `git diff
--check`. CI is skipped for this data-only change. SlopCop review is
skipped.

> AGENT GENERATED
## Human comments

## What was wrong

The timeline virtualizer treated large automatic or user scroll jumps as
fast traversal and unmounted visible row contents until a 300 ms idle
timeout. Crossing the virtualization threshold also swapped rendering
component trees, remounting existing rows.

## What changed

Keep visible virtualized row bodies realized during scrolling, with one
stable implementation/wrapper across the threshold. Retain normal rows
until the virtualizer has a usable range, capture heights before
activation, and retry nested scroll-root discovery. Scroll-anchor lookup
recognizes the stable wrapper in both modes. Offscreen rows still
unmount.

## How you verified

- 127 focused tests passed across 18 files: timeline windowing,
threshold DOM/input-state preservation, programmatic scrolling, height,
unread divider, row isolation, and scroll anchoring. App typecheck,
optimized build, and bundle gate passed.
- Persistent optimized `pnpm start:worktree` builds: [control at
comparison base
3bcb0ec](https://ymichael--22271.getbb.app/threads/thr_4qfn7ie4py)
and [branch at exact head
4932c1d](https://ymichael--24410.getbb.app/threads/thr_fxq22473h7).
- Real-product repro: expand the completed 24-sample turn, then scroll
up 800 px at 1280×900. Control blanked up to nine visible rows over 18
sampled frames (~300 ms); branch had no placeholder frames during the
equivalent scroll. Both histories contain actual Codex commentary and
shell results.
- Spontaneous streaming blanking was **not** reproduced end to end.
Natural streaming observation windows had no placeholder frames on
either build. The control used Codex 6-Luna and branch 6-Sol, so
cadence/content are not identical.
- Supplementary component fixture: simulated bottom-follow during
eight-row bursts reproduces blanking before the fix; three fixed bursts
had zero blank frames. This is not proof of real-product streaming
behavior.
- Full screenshots, measurement scripts, provenance, and limitations are
in HANDOFF.md in the originating BB thread storage, not committed to
this PR.

> AGENT GENERATED
## Human comments

## What was wrong

When bb stops bundling a built-in plugin, its installed row stays.
Startup (`reconcileBundled`) only walks plugins that are currently
bundled: it installs new ones and never looks at rows whose built-in is
gone. After get-bb#4232 reverted get-bb#4229/get-bb#4230, data dirs that had run those
builds kept `bb-ai` and `bb-account` forever, showing as "Missing:
Plugin files are missing. Update or reinstall bb." The delete route
refuses built-ins, so users couldn't remove them either. get-bb#4240 made the
manual uninstall possible; this makes cleaning them up by hand
unnecessary.

## What changed

`apps/server/src/services/plugins/plugin-service.ts`:
- After bundled reconciliation and before `loadAll()`, startup deletes
every installed built-in whose name is no longer in this build's
registry. It uses the existing orphan check behind `isOrphanedBuiltin`,
now a shared `isOrphanedBuiltinRow` helper.
- It also deletes the plugin's settings, secrets and schedules through
the same helper manual removal uses (`deleteRemovedPluginData`,
extracted from `remove()`). That includes `onPluginUnregistered`, which
re-dispatches queued messages the plugin was holding.
- The row is deleted outright rather than tombstoned, so a plugin that
is bundled again later installs fresh.
- Startup deletes the row only after settings, schedules, and secret
cleanup succeeds. If secret removal fails, the row remains so the next
startup retries cleanup.
- Rows a user already uninstalled (tombstoned) are left alone, because
`listInstalledPlugins` skips them.
- Each removal is logged: `plugin <id> removed because bb no longer
bundles builtin:<name>; its settings, secrets, and schedules were
deleted`.
- The check compares against the static registry, not what loaded, so a
broken or partial build can't delete bundled plugins.

No wire or daemon protocol changes. No CLI or UI changes.

## How you verified

- `builtin-plugins.test.ts`: the old "marks a persisted builtin as
orphaned" test is replaced by "removes a builtin and its data once bb no
longer bundles it". It seeds a setting, a schedule and a secret file,
restarts without the built-in, and asserts the row, settings, schedule
and secrets directory are gone and the list is empty. It then re-bundles
and asserts a fresh, enabled install. It fails with the startup call
removed and passes with it.
- `builtin-plugins.test.ts` also simulates one secret removal failure,
confirms the registration remains, then confirms a second startup
removes the secret and row.
- `pnpm exec turbo run test --filter=@bb/server -- --run
test/services/plugins/builtin-plugins.test.ts`: 35/35.
- `pnpm exec turbo run typecheck --filter=@bb/server`: clean.
- `pnpm exec turbo run typecheck lint --filter=@bb/server`: clean.
- `turbo run test --filter=@bb/server -- test/services/plugins`:
521/521.
- Live, in a disposable `pnpm start:worktree` instance whose data dir
really went through get-bb#4230 then the revert, so it had `bb-ai` and
`bb-account` rows with their folders missing, plus one seeded `bb-ai`
setting. After restarting on this branch, both rows and the setting were
gone, the API listed no orphans, `bb-guide` was unchanged, and the
server log had both removal lines.

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

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Saved servers take an optional name in the existing Add Server and Set
Server URL dialogs, shown in Window → Server instead of the host. A
Show This Mac checkbox hides the built-in entry while another server is
selected. Set Server URL edits a server in place, keeping its position
and name. The quick palette lists the other servers through optional
BbDesktopApi members that expose names and opaque ids, not URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Names and the Show This Mac choice move to server-menu.json, so
server-target.json keeps the exact format released builds read and
write. Parsing now ignores unknown fields instead of discarding the
whole file, which a later save then overwrote. A missing or corrupt
server-menu.json falls back to defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Set Server URL with an unchanged URL now only renames the server instead
of selecting it and reloading every window. Clearing the current server
while This Mac is hidden falls back to the next saved server. Editing a
URL onto another saved server keeps that server's name. Saves are
serialized so overlapping writes cannot leave an older state on disk,
one malformed name no longer drops the others, and loaded names use the
dialog's whitespace rules. The menu refreshes as soon as the dialog
saves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@idrevnii
idrevnii force-pushed the bb/desktop-server-manager branch from 42f5e38 to 67952f4 Compare September 24, 2026 20:58
@idrevnii

Copy link
Copy Markdown
Owner Author

Superseded by get-bb#4296.

@idrevnii idrevnii closed this Sep 24, 2026
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.

3 participants