Skip to content

Sort Browse Plugins by install count, and default to it - #1

Closed
andrewkchan wants to merge 232 commits into
mainfrom
browse-plugins-sort-by-installs
Closed

Sort Browse Plugins by install count, and default to it#1
andrewkchan wants to merge 232 commits into
mainfrom
browse-plugins-sort-by-installs

Conversation

@andrewkchan

Copy link
Copy Markdown
Owner

What was wrong

get-bb#2282 published install counts on every Browse card, but the sort menu
still offered one option, "Plugin name". The store's only popularity
signal was per-card text the user had to scan for, and the grid opened
alphabetically — so a widely adopted plugin appeared wherever its name
landed.

What changed

apps/app/src/components/plugin/management/BrowsePluginsTab.tsx:

  • The sort menu gains an "Installs" option, and Browse now opens on it,
    descending: a store's first screen should be the plugins people
    actually install. Alphabetical stays one click away.
  • groupByPublisher takes the mode. Install order sorts numerically,
    with entries the sidecar does not name sinking to the bottom in both
    directions — an unpublished count is unknown, not zero — and names
    breaking ties so equally installed plugins stay stable.
  • Only the curated marketplace publishes counts, so a catalog with none
    disables the option and falls back to alphabetical ascending, rather
    than inheriting the count sort's descending direction and showing an
    unexplained Z→A grid. changeSort compares against the mode on
    screen, so the checked row always toggles direction.

No wire, CLI, or doc surface changes: this is a view affordance over
data the API already returns, and bb plugin search already prints an
Installs column (apps/cli/src/commands/plugin.ts:918).

How you verified

Two tests in BrowsePluginsTab.test.tsx, both failing before this
change: install-count ordering (default mode and direction on first
render, the uncounted entry pinned last in both directions, and the
reset to ascending when switching back to names), and the disabled
option plus alphabetical fallback when no listing publishes a count.

  • pnpm exec turbo run test --filter=@bb/app -- BrowsePluginsTab — 13/13
  • pnpm exec turbo run typecheck --filter=@bb/app — clean

AGENT GENERATED

ymichael and others added 30 commits August 19, 2026 22:36
## What was wrong

The UI reused the server-resolved primary host as the meaning of “This
machine” in Machines and Updates. On remote and multi-host clients, that
could label an execution default or server host as the device running
the client. The primary-host fallback could also leak into removal
policy.

## What changed

- Drive “This machine” only from the daemon reachable on the client
device, and suppress the badge when only one host is known.
- Keep authoritative primary-host policy and primary markers separate
from client-local identity.
- Avoid promoting the first-connected fallback into primary-host removal
policy.
- On mobile, use “Primary” only where multi-host disambiguation is
useful and never claim the phone is a bb machine.
- Clarify nearby copy that means the selected or primary machine. No
server/daemon wire contract changed, so the protocol version is
unchanged.

## How you verified

- `pnpm exec turbo run test --filter=@bb/app --
src/components/settings/MachinesSettingsSection.test.tsx
src/views/MachineSettingsView.test.tsx
src/components/settings/UpdatesSettingsSection.test.tsx` (48 tests
passed)
- `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/mobile`
- `pnpm exec turbo run test --filter=@bb/app --filter=@bb/mobile` (3,858
tests passed, 3 skipped)
- `pnpm exec turbo run lint --filter=@bb/app --filter=@bb/mobile` (0
errors; existing warnings only)
- `git diff --check`

Fixes: N/A (no linked issue)

> AGENT GENERATED: by GPT-5
## What was wrong

The first-run setup guide shipped behind the `newOnboarding` experiment,
which defaults to false (`packages/domain/src/experiments.ts`) and was
never enabled by default. Nobody saw the flow unless they turned the
toggle on by hand, so its UI, its telemetry funnel, its persisted
completion timestamp, and a dedicated host-daemon command existed to
serve a surface that never ran — spread across seven packages.

## What changed

Deleted the experiment and everything that existed only for it:

- `OnboardingHost` / `OnboardingFlow` (828 lines) and their `App.tsx`
mount.
- The `newOnboarding` experiment key, its Settings → Experiments toggle
(web and `apps/mobile`), and every config fixture that listed it.
- `appSettingsSchema.onboardingCompletedAt`, the Settings → General
"Setup guide" replay control, and `bb settings replay-onboarding`. No
migration is needed: since get-bb#102 app settings are key/value rows read
through `inArray(key, appSettingsKeys)`, so the retired key's row is
ignored. The legacy wide `app_settings` table keeps its column, which is
deliberately frozen for `seedKeepAwakePluginConfiguration`.
- The five onboarding funnel telemetry events, `POST
/system/onboarding/event`, and `sdk.system.onboardingEvent`.
- `GET /system/onboarding/repos`, `sdk.system.onboardingRepos`, and —
since nothing else sent it — the `workspace.discover_repos` daemon
command with its 460-line handler.

**Wire change: `HOST_DAEMON_PROTOCOL_VERSION` 137 → 138**, because a
command left the protocol. The reason is recorded in the `protocol.ts`
header.

Docs and agent surfaces updated in the same change:
`bb-guide-customization.md` (regenerated), the `bb-cli` SKILL and its
`app-settings` reference, `bb-plugin-authoring`'s SDK method table, and
`docs/configuration.md`.

`getOnboardingAgentOverview` and `GET /system/onboarding/agents`
deliberately stay. Despite the name they are not onboarding-only: the
root composer resolves an unset provider selection through
`useOnboardingAgents` (`useThreadCreationOptions.ts` ←
`RootComposeView.tsx`), which ships unconditionally. Renaming that
endpoint and its SDK method to match its real purpose is a follow-up,
not part of a deletion. `useOnboardingAgents` does lose its `poll`
option, which became constant once the flow's caller went away.

Two test adjustments worth calling out, both because their subject was
deleted rather than because they broke:

- `apps/cli` had a test using `onboardingCompletedAt` as its
nullable-string case for `bb settings general`. `appSettingsSchema` now
holds only booleans, so that path has no live key to exercise; the test
is trimmed to the unknown-key assertion it also covered. The generic
value-parsing code in `updateGeneralSetting` is untouched and still
accepts `null`.
- `packages/db`'s key/value migration test no longer asserts a settings
field that no longer exists.

## How you verified

Nothing here is a behavior fix, so the evidence is that removing a
never-enabled surface changes nothing else:

- `pnpm exec turbo run typecheck` — 72/72 tasks green.
- `pnpm exec turbo run test` for `@bb/app` (3037), `@bb/server` (1787),
`@bb/mobile` (812), `@bb/host-daemon` (47 files), plus `@bb/db`,
`@bb/domain`, `@bb/sdk`, `@bb/cli`, `@bb/desktop`,
`@bb/server-contract`, `@bb/host-daemon-contract`, `@bb/templates`,
`@get-bb/plugin-sdk` — all green.
- `pnpm exec turbo run lint` — 0 errors (144 pre-existing react-compiler
warnings in `@bb/app`, unchanged).
- `packages/host-daemon-contract`'s protocol-version test pins 138, and
its command-fixture map no longer accepts `workspace.discover_repos`, so
a stray sender fails the contract test.
- Repo-wide grep for `newOnboarding`, `onboardingCompletedAt`,
`replay-onboarding`, `onboardingRepos`, `onboardingEvent`, and
`discover_repos` returns only the `protocol.ts` changelog note.

No CHANGELOG entry: per `docs/bb-release-process.md` those sections are
written at release-prep time, and this removes nothing a user could see.

Fixes: N/A — no linked issue.

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…get-bb#2009)

## What was wrong

The mobile app stacked two headers on the thread screen (the native bar
plus a second title / status / environment / actions block), new threads
went through a separate `/compose` page, and the composer always showed
its full pill rows and a context readout. Picker sheets clipped their
last row under the home indicator because the `scroll` layout rendered
the title outside the measured scroll view, so dynamic sizing came up
short by the header height.

## What changed

- **Thread screen**: one native header — title (tap → rename) with a
status subtitle only while working / needs input / error, panel + `…` on
the right. Environment line, child roll-up, Workspace and the git action
live in the `…` sheet (`leadingActions` / `headerDetail`). Table of
contents removed end to end.
- **Composer**: `collapsible` / `topControls` / `onExpandedChange`.
Collapsed = `[+] placeholder [mic | Stop while running]`; expanded ⇔
focused (or has content). Sheets opened from inside report presence
through `SheetPresenceContext` so the card stays open and refocuses
after a picker. Context-window readout is a ring shown only at ≥60%.
`PickerTrigger` is ghost by default. Pill text nudged up 3pt on iOS to
centre on the buttons.
- **Home**: `ComposeDock` replaces the FAB and the `/compose` route. It
expands in place over a scrim; the drawer header is painted the same
blend (`blendOver`) so the whole screen dims. `composeHref` →
`newThreadHref` (home with params + `newThread=1`); drawer row, project
"+", fork, handoff, new project, share intent and `bb://compose` all
route there; home reads/clears the params. Controller re-seeds per param
change and drops the dead `title` knob. Thread rows drop their
timestamps (`relative-time` removed).
- **Sheets**: the `scroll` layout keeps the title as a sticky first
child inside `BottomSheetScrollView` so dynamic sizing includes it; the
provider CLI log sheet pads `insets.bottom`.
- Also removes the stale `claudeCodeMockCliTraffic` experiment row
(pre-existing typecheck break).

## How you verified

- `pnpm exec turbo run typecheck lint test --filter=@bb/mobile` green
(812 tests; new tests for `blendOver`, `/compose` link mapping).
- Maestro against the harness backend on a Debug dev client:
`phase1-shell`, `phase3-compose`, `phase4a-timeline`, `phase4b-send`,
`phase4b-actions`, `phase4b-thread-actions`, `phase4b-queue`,
`phase4b-approve`, `phase4b-ask-user`, `phase6-panel` pass (flows
updated for the moved controls and the pill-until-focused composer).
- Visual checks in the iPhone 17 Pro simulator, light and dark: home
pill → card, picker keeps it open, scrim + header dimming, create →
thread, fork → dock with hint, thread header + menu, picker sheets with
full content and bottom inset.

Fixes #N/A

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong

The [failing main CI
run](https://github.com/get-bb/bb/actions/runs/32336675998) passed all
1,089 app-shard assertions, then reported an unhandled
`scrollContainer.removeEventListener is not a function` exception from
`PromptBoxInternal.test.tsx`. TipTap React intentionally destroys editor
instances on a 1 ms timer after unmount so Strict Mode can reuse them.
The suite returned from `afterEach` immediately after React cleanup,
allowing the final delayed editor destruction to race Vitest jsdom
environment shutdown on a loaded CI worker.

## What changed

Wait for TipTap deferred editor destruction after cleaning up each
`PromptBoxInternal` test. This keeps editor and scroll-listener teardown
inside the jsdom environment that created them and prevents state from
leaking across tests or into environment shutdown. There are no product,
wire-protocol, CLI, guide, or documentation changes.

## How you verified

- Fail-before evidence: main CI app shard completed 132 files and 1,089
assertions, then failed during delayed TipTap destruction.
- `pnpm exec turbo run test --filter=@bb/app --force --
src/components/promptbox/PromptBoxInternal.test.tsx` — 99 tests passed.
- `pnpm exec turbo run test --filter=@bb/app --force -- --shard=3/3`
under concurrent typecheck load — 132 files and 1,089 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/app` — passed.
- `git diff --check` — passed.

Fixes: N/A — CI teardown flake.

> AGENT GENERATED: by GPT-5
## What was wrong

When Claude Code resumed a session that had orphaned background work,
the SDK could drain a zero-work `result` with
`origin.kind=task-notification` immediately before processing the newly
queued human prompt. The Claude bridge emitted `input.accepted` as soon
as it queued that prompt, and the translator allowed any result to claim
a pending accepted input. The recovered notification therefore
manufactured and completed an empty turn for the new message; the real
answer continued later under a second, unaccepted turn. This is the
mechanism independently reproduced in
[get-bb#1718](get-bb#1718) and observed again in
[the affected production
thread](https://ymichael.getbb.app/projects/proj_qrv2s45kmq/threads/thr_gcuc46ug4j).

## What changed

- Parse Claude result provenance and allow an idle result to claim
pending input only when its origin is human (or omitted, which the SDK
defines as human). A non-human result can still settle work when a turn
is already open, preserving live background-notification loops and human
zero-work commands such as `/clear`.
- Make Claude `turn/start` match `turn/steer`: emit acceptance and
answer the command only after the SDK prompt iterator consumes the
queued input.
- Add regressions for the recovered-task-notification sequence and for
turn/start consumption ordering. Existing bridge tests now follow the
same consumed-before-accepted contract.
- Bump `HOST_DAEMON_PROTOCOL_VERSION` from 138 to 139 because older
daemons emit the incorrect lifecycle semantics.

The uniform provider rule is: `input.accepted` means the provider
consumed the input, never merely that bb queued it. Codex already
follows that rule. Pi's prompt promise currently reports settlement
rather than consumption, and ACP exposes no equivalent provider
acknowledgement, so those bridges need provider-specific correlation
work rather than a timing heuristic in this focused Claude fix. The
Pi/ACP cross-provider follow-up is tracked in
[get-bb#2014](get-bb#2014). There are no CLI,
guide, configuration, or user-facing documentation changes.

## How you verified

- Before the implementation, the new provenance regression received
`turn/started` + `turn/input/accepted` + `turn/completed` instead of no
events, and the new turn/start ordering regression observed a response
before SDK consumption.
- `pnpm exec turbo run test --filter=bb-plugin-provider-claude-code
--force` — 259 tests passed, including `/clear` zero-work conformance.
- `pnpm exec turbo run typecheck --filter=bb-plugin-provider-claude-code
--force` — passed.
- `pnpm exec turbo run test typecheck --filter=@bb/host-daemon-contract
--force` — 52 tests passed; typecheck passed.
- `pnpm exec turbo run typecheck --filter=@bb/host-daemon --force` —
passed.
- `pnpm exec turbo run typecheck --filter=@bb/server --force` — passed.
- `git diff --check` — passed.

Fixes get-bb#1718

> AGENT GENERATED: by GPT-5
## What was wrong

Sent-message editing had three related compatibility assumptions:

- After the narrow provider-bridge grammar moved canonical timeline
assembly into the runtime, bb turn IDs and native Codex turn IDs became
intentionally different, but the server still sent the bb timeline ID as
the Codex rewind checkpoint.
- The Codex bridge only persisted checkpoints for successfully completed
turns, even though Codex also persists and accepts interrupted turn IDs
as fork boundaries. Editing after a stopped turn therefore fell back to
a bb ID such as `da2f291120-t3`, which Codex correctly rejected as
absent from its source thread.
- The web client called `crypto.randomUUID` directly even though some
supported browser contexts expose Web Crypto `getRandomValues` without
`randomUUID`, causing the edit action to throw before the editor opened.

The failures were reproduced from the originally reported thread and
from the interrupted-turn repro in `thr_kfzu8tqu2d`.

## What changed

Codex rewinds now use the persisted native `providerCheckpointId`. The
compatibility fallback is restricted to UUID-shaped turn IDs from legacy
Codex timelines, so a runtime-minted bb ID can never be forwarded to
Codex.

The Codex bridge now persists the native checkpoint for completed and
interrupted `turn/completed` statuses. This makes edits after a stopped
Codex turn use the fork point Codex actually emitted. Failed turns
remain unstamped because older Codex rollouts may omit them and no
equivalent fork proof exists for that status.

Web edit sessions now create operation IDs through `nanoid`, which is
already an app dependency and works when `crypto.randomUUID` is
unavailable. ID generation remains inside the edit click handler.

These changes populate and validate the existing `providerCheckpointId`
/ `retainThroughProviderCheckpoint` fields; they do not change the
server/daemon wire contract, so `HOST_DAEMON_PROTOCOL_VERSION` is
unchanged. There are no CLI, guide, or configuration changes.

## How you verified

- Direct `codex app-server` `thread/read` showed the interrupted native
turn ID, and an ephemeral `thread/fork` using it as `lastTurnId`
succeeded.
- Before the original server implementation change, the focused suite
failed 8 Codex cases with `checkpoint-first` expected and `turn-first`
received.
- `pnpm exec turbo run test --filter=@bb/server -- --run
test/threads/thread-edit-message.test.ts` — 42 passed.
- `pnpm exec turbo run test --filter=bb-plugin-provider-codex -- --run
src/delta-translation.test.ts src/bridge/bridge.zero-work-turn.test.ts`
— 61 passed, including the full `thread/stop` → interrupted completion
checkpoint path.
- `pnpm exec turbo run typecheck --filter=@bb/server` — passed.
- `pnpm exec turbo run typecheck --filter=bb-plugin-provider-codex` —
passed.
- Before the web implementation change, the compatibility test failed
with `TypeError: crypto.randomUUID is not a function`.
- `pnpm exec turbo run test --filter=@bb/app --force -- --run
src/views/thread-detail/sent-message-edit-operation-id.test.ts` —
passed.
- `pnpm exec turbo run typecheck --filter=@bb/app` — passed.
- `pnpm exec turbo run lint --filter=@bb/app` — passed with 0 errors and
144 existing warnings.

Fixes the reported sent-message edit failures.

> AGENT GENERATED: by GPT-5
## What was wrong

React Strict Mode in the development app replays Pierre's ref callback
against the retained diff custom element. The replacement renderer
hydrates the existing plain `<pre>`, records it as already highlighted,
and starts a worker task. Its normal render then early-returns because
the file is unchanged. When the worker returns the highlighted AST,
Pierre suppresses the repaint because the hydrated cache says it is
already highlighted. Production does not perform this simulated remount,
which is why `pnpm start` worked while `pnpm dev` did not.

The packages CI job also exposed a separate Connect test race: the
connected status renders before the asynchronous mobile-pairing
capability response, but three tests synchronously queried the
capability-gated button.

## What changed

Added a development-only host adapter around Pierre's public
`onPostRender` and `rerender()` APIs. A mount schedules one microtask
after React's ref replay: the discarded renderer has already been
disabled and safely no-ops, while the retained renderer takes Pierre's
normal forced-render path so its worker completion can repaint. The
adapter preserves plugin callbacks and stable option identities, covers
the plugin `File`, `FileDiff`, `MultiFileDiff`, `PatchDiff`, and
`UnresolvedFile` surfaces, and also covers the built-in diff card.
Production receives the original options object unchanged.

Removed the `pnpm` patched dependency and its renderer-internals
regression; the install now uses stock `@pierre/diffs` 1.2.9. Added a
host-level regression for the Strict Mode replay ordering. The Connect
tests now await the mobile-pairing button before interacting with it.
This is frontend/test-only; there are no host daemon protocol, CLI, or
documentation changes.

## How you verified

- The new recovery regression models the discarded and retained
renderers and verifies that repaint waits until after ref replay.
- `pnpm install --frozen-lockfile --ignore-scripts` passed with
unmodified `@pierre/diffs` 1.2.9.
- `pnpm exec turbo run test --filter=@bb/app --force`: 399 files / 3,041
tests passed (3 skipped).
- `pnpm exec turbo run test --filter=@bb/app --filter=bb-plugin-github
--filter=bb-plugin-connect --force`: app, GitHub (21 tests), and Connect
(90 tests) passed.
- `pnpm exec turbo run typecheck --filter=@bb/app
--filter=bb-plugin-github --filter=bb-plugin-connect --force`: passed.
- `pnpm exec turbo run lint --filter=@bb/app --force`: passed with 0
errors (pre-existing warnings only).
- `pnpm exec turbo run build --filter=@bb/app --force`: passed.
- Sawyer Hood's dev-browser against a real GitHub pull request under
`pnpm dev` found 39 syntax-token spans with 6 distinct token styles in
the original reproduction file.
- Reproduced the Connect failure locally before awaiting the
capability-gated button; its complete 90-test suite passes afterward.
- `git diff --check`: passed.

Fixes: development-only GitHub plugin diff syntax highlighting

> AGENT GENERATED: by GPT-5
…op button (get-bb#2015)

## What was wrong

Four mobile polish issues from dogfooding on a phone:

- Every dimming overlay (home compose scrim, sheet backdrops) used
`tokens.ink` at 35%. Dark palettes have a light `ink`, so the overlay
lightened the screen to gray instead of dimming it.
- `KeyboardPaddingView` subtracted the full home-indicator inset when
the keyboard opened, so composers sat flush against the keyboard.
- In the flat thread list the project name under a title was plain text
and read like a second title.
- The collapsed composer's Stop control was a square `secondary` button
with a stroked square icon, flush with the pill edge.

## What changed

- `apps/mobile/src/theme/scrim.ts`: `scrimBaseColor(mode, tokens)` —
`ink` in light mode, black in dark mode. Used by the home compose scrim
+ dimmed header (`HomeScreen.tsx`), the bottom-sheet backdrop
(`ui/Sheet.tsx`), and the navigation drawer overlay
(`app/(drawer)/_layout.tsx`).
- `ui/KeyboardPaddingView.tsx`: new `keyboardGap` prop and
`COMPOSER_KEYBOARD_GAP = 8`; applied to the home dock, thread composer,
and composer showcase.
- `screens/sidebar/SidebarRows.tsx`: thread-row subtitle is now
`{kind:"project"} | {kind:"snippet"}`; project subtitles render a
`Folder` icon. Search snippets stay plain. Archived/search screens
updated.
- `composer/Composer.tsx`: `StopButton` — a 36pt round `secondary`
circle with a filled square, used in both the collapsed pill and the
expanded footer.

## How you verified

- `pnpm exec turbo run typecheck lint --filter=@bb/mobile` pass.
- New `scrim.test.ts` asserts the scrim darkens `background` for every
palette × mode (fails with the old `ink` scrim in dark mode).
- Simulator (iPhone 17 Pro, dark mode) against the local dev server:
home compose scrim, display-options sheet backdrop, keyboard gap,
project folder subtitle, and collapsed Stop button during a live turn.
- Release build installed on a physical iPhone for the scrim/gap/folder
changes.

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong

A remote bb page received only the primary server host's editor-helper
port, then probed that port on the browser's own loopback interface. On
an enrolled secondary machine, the matching local daemon commonly
listens on another port (for example `38888`), so Settings → Files could
not reach it. The helper also required a duplicate remote-origin
setting, and SSH target configuration could not select a host when the
server had multiple machines.

## What changed

- Report each daemon's full local API port during session open and
expose the deduplicated connected-port candidates in system config.
- Probe advertised candidates in parallel only after local-network
access is available, selecting the helper whose reported server origin
matches the current page. If none responds, retry the same candidates
twice at one-second intervals.
- Automatically allow the exact enrolled server origin in the local
helper's CORS policy.
- Add `--host-id` support to `bb-app client ssh-target set/remove`,
retaining single-host auto-selection and server-wide removal behavior
when omitted.
- Bump `HOST_DAEMON_PROTOCOL_VERSION` from 138 to 139 so enrolled
daemons update for the wire change.
- Update CLI help, the builtin bb CLI skill, configuration docs, and the
multiple-devices guide.

## How you verified

Added coverage that fails without the new behavior for daemon port
reporting, server candidate aggregation, exact-origin CORS access,
browser origin selection, the two one-second retries, and host-specific
SSH target configuration.

- `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/server
--filter=@bb/host-daemon --filter=bb-app`
- `pnpm exec turbo run test --filter=@bb/app -- --run
src/lib/local-host-daemon-access.test.ts
src/lib/system-config-atoms.local-access.test.ts`
- `pnpm exec turbo run test --filter=@bb/server -- --run
test/system/local-helper-ports.test.ts test/app/hub.test.ts`
- `pnpm exec turbo run test --filter=@bb/host-daemon -- --run
src/app.test.ts src/local-api.test.ts src/server-client.test.ts
src/server-connection.test.ts`
- `pnpm exec turbo run test --filter=@bb/host-daemon-contract -- --run
test/contract.test.ts`
- `pnpm exec turbo run test --filter=bb-app -- --run test/index.test.ts`
- After rebasing onto `main`: `pnpm exec turbo run test --filter=@bb/app
-- --run src/lib/local-host-daemon-access.test.ts
src/lib/system-config-atoms.local-access.test.ts
src/components/settings/MachinesSettingsSection.test.tsx
src/views/MachineSettingsView.test.tsx
src/components/settings/UpdatesSettingsSection.test.tsx` (63 tests
passed)
- Prettier checks and `git diff --check`

Fixes: N/A (no linked issue)

> AGENT GENERATED: by GPT-5
…et-bb#1876)

## What was wrong

BB rendered code in two independent places and had no way for a plugin
to change either. The file preview drove `@pierre/diffs`' `File` view
and the diff card drove its `FileDiff` view, each assembling its own
options record, its own line-selection wiring, and its own code-theme
lookup. A plugin that wanted to render source or a diff had a third
path: import `@pierre/diffs` directly through the runtime shim and
rebuild the host's behavior by hand. `plugins/github` did exactly that —
synthesizing a `diff --git` header for GitHub's REST patches, calling
`parsePatchFiles`, and running two MutationObservers (one on the root
`class`, one on `data-bb-code-theme-*`) to keep Pierre's theme in step
with BB's.

That is three copies of one capability, and it made "change how bb
renders code" impossible to express: there was nothing to replace.

## What changed

**One host boundary per capability.** `SourceCodeHost` and `DiffHost`
(`apps/app/src/components/code/`) resolve an exclusive plugin
replacement through the same `resolveReplacement` +
`PluginReplacementSlot` path the sidebar thread list and file openers
already use, and otherwise render BB's own renderer. Both BB renderers
(`BbSourceCode`, `BbDiff`) sit behind `lazy()`, so a replacement that
never delegates never downloads them and `experimental_Original` costs
nothing until it is rendered.

**Public API** (all `experimental_`, with entries in
`docs/api_to_audit.md`):

```ts
experimental_SourceCode: { content, path, overflow?, highlightedLines?, className? }
experimental_Diff:       { patch, path, view?, overflow?, showLineNumbers?, className? }

app.slots.experimental_sourceCodeRenderer({ id, title, description?, component })
app.slots.experimental_diffRenderer({ id, title, description?, component })
```

A replacement receives fully resolved semantic props plus a bound
`experimental_Original`, so it can delegate per call without re-entering
resolution. Host-only inputs — the pre-parsed `ParsedGitDiffFile`, the
raw patch text, the highlighter cache key, selection-to-composer — never
cross the boundary, and no `@pierre/diffs`, Shiki, `FileOptions`, or
`ParsedGitDiffFile` type appears in the public contract.
`experimental_Diff` owns patch normalization, so a patch with no `diff
--git` header (GitHub REST, a bare `@@` hunk) renders without the caller
synthesizing one; content that parses to no hunks degrades to plain
monospace text instead of an empty diff.

**Migrations.** The native file preview, timeline file diffs, and the
environment diff panel's file bodies all render through the boundary, so
one registration covers BB's surfaces and plugin surfaces alike.
`plugins/github` renders `experimental_Diff` and drops its
`@pierre/diffs` devDependency along with both MutationObservers; its
bundle no longer references the Pierre runtime shim (417.0 KB → 409.0
KB). The shim itself stays for compatibility with existing plugins.

**User control.** Settings → Appearance gains **Source code** and
**Diffs** rows beside **Sidebar** — Automatic / bb (built-in) / each
registered provider, per client, each row hidden when no plugin supplies
that renderer. A renderer takes over surfaces there is otherwise no
route back from, so this is what keeps "installing activates it"
reversible. `lib/plugin-replacement-preference.ts` now owns that
automatic/built-in/named-provider rule and the thread list consumes it
too, retiring its private copy and the `resolveThreadListReplacement`
wrapper only its own test still called. Registered renderers also appear
in the plugin detail's capability list.

**Two cleanups fell out.** The opaque `diffViewOptions: Record<string,
string | boolean | number>` threaded through five components became the
semantic `DiffPresentation` (`view` / `overflow` / `showLineNumbers`).
The renderers own light/dark selection themselves, which made the
`themeType` prop chain from `ThreadTimelineSurface` down to
`TimelineFileDiffBlock` dead; it and `ThreadTimelineTheme` are removed.

**Docs and generated artifacts.** `docs/api_to_audit.md` gains three
entries (components, slots, `experimental_Original`); the
`bb-plugin-authoring` skill and the `bb guide` plugins chapter now point
at the host components rather than a direct `@pierre/diffs` import, and
the skill's own coverage test is extended so the new slots and their
props stay documented. Bundled SDK declarations, the runtime export
manifest, and the templates bundle were regenerated with the repository
scripts. No wire changes, so no `HOST_DAEMON_PROTOCOL_VERSION` bump.

## How you verified

Regression coverage for what the boundary actually promises — each of
these fails without the corresponding behavior:

- a replacement that never delegates leaves BB's renderer chunk
unloaded; delegating through `experimental_Original` loads it;
- the replacement receives resolved semantic props only — no parsed
file, no cache key, no selection-to-chat;
- with no patch text in hand the host reconstructs a single-file patch
that re-parses to the same file;
- crash and no-registration paths land on BB's renderer;
- BB's diff renderer follows `applyResolvedCodeTheme` live — the
behavior `plugins/github` previously got from a DOM MutationObserver;
- `DiffFileCard`, a first-party surface, renders its text body through
the same boundary;
- a built-in pin renders BB's renderer with the plugin still installed
and enabled, and an explicit pin survives a later plugin whose id sorts
ahead of it (where automatic selection would silently swap the user's
renderer).

Commands run:

- `pnpm exec turbo run typecheck --filter=@bb/app
--filter=@get-bb/plugin-sdk --filter=@bb/plugin-build
--filter=@bb/templates --filter=@bb/server --filter=bb-plugin-github
--filter=@bb/cli` — passed.
- `pnpm exec turbo run test --filter=@bb/app` — 367 files / 2909 tests
passed.
- `pnpm exec turbo run test --filter=@bb/server` — 182 files / 1730
tests passed.
- `pnpm exec turbo run test --filter=@get-bb/plugin-sdk
--filter=@bb/plugin-build --filter=@bb/templates
--filter=bb-plugin-github --filter=@bb/cli` — 631 tests passed.
- `pnpm exec turbo run lint --filter=@bb/app` — 0 errors (the
repository's existing 147 react-compiler warnings; several are carried
over verbatim with the extracted renderer code).
- `pnpm exec turbo run build --filter=bb-plugin-github` — builds; the
bundle contains `experimental_Diff` and no Pierre shim reference.

**Bundle impact.** Boot payload 1655.6 → 1659.1 KB raw and 448.8 → 453.7
KB brotli against the 1671.0 / 456.4 budget; 40 → 43 chunks. I confirmed
this is not new code on the boot path — stubbing out the patch
reconstruction moved it by 0.3 KB — it is the extra chunks compressing
slightly worse than fewer larger ones. All `forbiddenBootPackages` still
pass. New lazy chunks: `BbDiff` 2.6 KB raw / 1.1 KB brotli,
`BbSourceCode` 6.1 KB raw / 2.4 KB brotli. Brotli headroom drops from
~7.6 KB to ~2.7 KB, which is worth knowing before the next feature lands
on that budget.

**Deliberately not migrated.** Markdown fenced code blocks are genuine
source but run `sugar-high` — a synchronous ~2 KB highlighter with no
worker pool, no shadow DOM, and no async settle, which is what a
streaming chat timeline needs. Routing them through the host would
either regress that or require a second per-surface default renderer,
contradicting one host per capability. Raw-log surfaces
(`EventCodeBlock`, terminal output) are monospace styling, not code
rendering, and are untouched.

**Open questions for review**, all recorded in `docs/api_to_audit.md`:
whether a per-client pin is the right scope for something as visible as
every diff (and whether users expect one combined choice rather than
two); whether a plugin should be able to change how *another* plugin's
`experimental_Diff` renders; and whether the silent crash fallback is
right where the thread list toasts.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What was wrong

The new-thread composer treated two background discovery requests as
hard submission prerequisites. Connected-provider discovery is
host-keyed, so changing machines restarted an expensive onboarding probe
and disabled an otherwise complete composer; it could also silently
change the automatic provider choice. Project branch metadata loading
also disabled managed-worktree submission even though the create path
independently resolves and validates the default base branch on the
selected host. The composer additionally collapsed all remaining
blockers into one unexplained disabled boolean.

## What changed

Connected-provider discovery is now used only to establish the initial
automatic provider default. Its first settled result is retained across
machine switches, and the background probe no longer gates submission.
Managed-worktree submission can now proceed with `{ kind: "default" }`
while branch metadata is loading; the server resolves that default
authoritatively during thread creation. A confirmed non-Git or
commitless project source still disables managed-worktree creation, and
the branch query still enriches the branch picker.

The composer also resolves the remaining legitimate eligibility checks
into a prioritized disabled reason. The prompt submit action exposes
that reason through its accessible label and a tooltip on a
pointer-capable wrapper. This is an app-only behavior change with no
host-daemon wire, CLI, guide, or documentation changes.

## How you verified

- Added hook coverage proving that switching machines retains the
initial connected-provider selection and does not issue another
onboarding probe; this fails against the previous behavior.
- Added coverage proving that a managed-worktree request can use the
server-resolved default while branch metadata is absent.
- Added resolver and prompt-box interaction coverage for the remaining
disabled reasons and tooltip behavior.
- `pnpm exec turbo run test --filter=@bb/app --
src/hooks/useThreadCreationOptions.test.tsx
src/views/RootComposeView.test.ts
src/views/root-compose-thread-environment.test.ts
src/components/promptbox/PromptBoxInternal.test.tsx` — 197 tests passed.
- `pnpm exec turbo run typecheck lint --filter=@bb/app` — all tasks
passed; lint reported zero errors and the existing warning baseline.
- `git diff --check` — passed.

Fixes: no linked issue.

> AGENT GENERATED: by GPT-5
## What was wrong

Two async transitions could suppress Edit after submitting a new thread.
Provider capability gating waited on the full
execution-options/model-discovery path, and navigation could briefly
lose the provider facts already loaded by the composer. More
importantly, the timeline controller preserved row identity using only
the row ID and source-sequence range. The server projects
`turnRequest.status` from `pending` to `accepted` onto that same message
row without extending its sequence range, so the merge retained the
stale pending object indefinitely. Edit requires an accepted message;
refresh rebuilt the timeline directly from the accepted server row and
made the icon appear.

## What changed

The thread detail view now reads capabilities from the lightweight,
environment-routed provider roster and reuses composer-warmed provider
facts while that roster loads. The timeline merge also includes
turn-request fields in its identity signature, so an accepted server
projection replaces the pending row instead of being discarded as
unchanged. Regression coverage exercises both the post-submit provider
fallback and the pending-to-accepted row transition. There are no wire,
CLI, guide, or protocol changes.

## How you verified

- Added a timeline-merge regression test that fails before the fix by
retaining `pending` and passes after the accepted row replaces it.
- Reproduced the exact flow in the browser: new thread, submit, navigate
to the active thread, and confirmed Edit appears without refresh while
the Stop run control is still present.
- `pnpm exec turbo run test --filter=@bb/client-core --force` (238
tests)
- `pnpm exec turbo run test --filter=@bb/app --force -- --run
src/hooks/queries/system-queries.test.tsx` (17 tests)
- `pnpm exec turbo run typecheck --filter=@bb/client-core
--filter=@bb/app`
- `pnpm exec turbo run lint --filter=@bb/client-core --filter=@bb/app`
(0 errors; existing warnings remain)
- `pnpm exec prettier --check` on the changed source files

Fixes: delayed edit-action visibility (no linked issue).

> AGENT GENERATED: by GPT-5
## What was wrong

[get-bb#2013](get-bb#2013) established the
uniform rule that `input.accepted` means the provider consumed the
input, never that bb queued it, because an acceptance still pending when
a stale terminal arrives lets that terminal claim the input and complete
an empty turn for a message the provider has not answered. Pi and ACP
still emitted acceptance at dispatch.

Pi's exposure is not just theoretical timing. `PiSdkSession.prompt()`
resolves as soon as pi queues a prompt that arrives while a run is still
unwinding, and the bridge reported that resolution as
`pi/prompt/settled` — a `claimIfIdle` turn terminal. So a `turn/start`
pi merely queued produced acceptance plus a terminal in the same tick,
which the assembler turned into a started-and-completed empty turn while
the real answer ran later under an unaccepted turn.

ACP emitted acceptance in the `turn/start` handler before the turn
opened, and for a steer it emitted acceptance at queue time even though
the queued input is dropped whenever the turn fails or the session stops
— reporting input the agent was never given as accepted into the turn.

## What changed

- `PiSdkSession` tracks pending input consumption for both of pi's
queues instead of steering only, and resolves it from pi's preflight
hook (the input entered a run) or from the queue update that delivers a
queued message. Its `prompt()` now returns that consumption signal
alongside the settlement of the run it started, and reports no
settlement for input pi queued into a run it did not start.
- The pi bridge answers `turn/start` and emits `input.accepted` only
once pi read the input.
- The ACP bridge carries the waiting command with the input and emits
`input.accepted` once the `session/prompt` request carrying it goes out,
so the acceptance names the open turn and a dropped steer is never
accepted. Every turn input still leaves with exactly one reply
([get-bb#853](get-bb#853)).
- `HOST_DAEMON_PROTOCOL_VERSION` 140 to 141: older daemons emit the
queue-time semantics and produce those phantom turns.

Two deviations from the issue's proposed fix:

1. The issue states pi's steer path "already waits for actual SDK
acceptance." It does not — `PiSdkSession.steer()` resolved once the SDK
took the message into its queue, the same queue-time violation as
`turn/start`.
2. Both steer paths deliberately keep answering their command at queue
time, because the runtime fails a bridge request that goes unanswered
for 30 seconds (`sendJsonRpcRequest`). Pi delivers steering only between
assistant turns, so a steer sent during a long tool call would time out;
ACP delivers a steer only when the cancelled prompt is reissued. Neither
can manufacture a turn: a steer's acceptance lands in a turn the
assembler already holds open, and the
[get-bb#2013](get-bb#2013) failure mode needs a
*pending* acceptance. Pi keeps reporting a steer its run never read
through the session error path.

There are no CLI, guide, configuration, or user-facing documentation
changes.

## How you verified

- New pi regression: a `turn/start` pi queues behind a live run emits no
turn events until the queue delivers it, then the acceptance lands in
the turn pi opened. Before the change it received `turn/started` +
`turn/input/accepted` + `turn/completed` — the phantom turn.
- New ACP regressions: acceptance is emitted immediately after the turn
opens rather than before it, and a steer dropped by `thread/stop` leaves
the turn with one accepted input instead of two. Both fail before, pass
after.
- New `PiSdkSession` coverage for queued-versus-direct dispatch, and for
a queued follow-up surviving the `agent_end` that continues into it.
- `pnpm exec turbo run typecheck test --filter=@bb/agent-runtime
--filter=bb-plugin-provider-acp --filter=@bb/host-daemon-contract
--force` — 417, 172, and 52 tests passed; typechecks passed.
- `pnpm exec turbo run build typecheck --filter='...[origin/main]'` — 62
tasks passed.
- `git diff --check` — passed.

Fixes get-bb#2014

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

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What was wrong

Cursor ACP applies its project MCP approval gate to client-supplied
session MCP servers. ACP has no client permission round trip for that
gate, so Cursor rejected the valid `bb-bridge` stdio config before
spawning it. The same config-advertisement path exists before and after
get-bb#1834, and the get-bb#1932 bootstrap fix remains valid; the missing Cursor
approval was the separate root cause.

## What changed

The ACP bridge now installs the exact bb-owned session MCP fingerprint
in the Cursor project approval store before `session/new`,
`session/load`, or `session/fork`. It limits the workaround to
`cursor-agent` plus the `bb-bridge` config, preserves existing
approvals, serializes concurrent updates, and removes approvals that bb
installed when the session ends.

The MCP child also reports `initialize` back to the bridge, giving
host-side diagnostics for both config construction and successful child
startup. No server/host-daemon wire contract changed, so
`HOST_DAEMON_PROTOCOL_VERSION` does not need a bump.

## How you verified

Added fingerprint, approval-file preservation/concurrency,
session-lifecycle, and MCP initialize diagnostic regressions. These
expose the missing approval before the fix and pass afterward.

- `pnpm exec turbo run test --filter=bb-plugin-provider-acp --force` —
175 passed
- `pnpm exec turbo run typecheck --filter=bb-plugin-provider-acp`
- Isolated manual run against Cursor CLI `2026.06.19-20-24-33-653a7fb`,
with approval installed after ACP `initialize` and before `session/new`;
Cursor spawned and initialized the MCP server

Fixes get-bb#2018

> AGENT GENERATED: by GPT-5
## What was wrong

Pi sampled context-window usage only on SDK `agent_end`, which fires
after the entire agent run. A tool-heavy run contains multiple SDK
`turn_end` events—each after an assistant response and its tool
results—so bb's context meter stayed stale throughout the tool loop even
though Pi's underlying context estimate was changing. See get-bb#2023.

## What changed

- Sample and emit Pi context-window usage on every `turn_end`, while
retaining the existing `compaction_end` update.
- Stop sampling again at `agent_end`; that event is still forwarded
normally for completion and checkpoint handling, but the final
`turn_end` already emitted the same context snapshot.
- Add a bridge regression test with an intermediate tool-result turn and
a final response. It asserts that both usage snapshots arrive and that
`agent_end` does not duplicate the final one.
- Bump `HOST_DAEMON_PROTOCOL_VERSION` from 141 to 142 because the
bundled Pi bridge's daemon-to-server event cadence changed and enrolled
daemons need to update.

## How you verified

- `pnpm exec turbo run test --filter=@bb/agent-runtime --force --
src/pi/bridge/__tests__/bridge.test.ts` — 27 passed. The new regression
fails before the fix because only the `agent_end` sample is emitted.
- `pnpm exec turbo run typecheck --filter=@bb/agent-runtime
--filter=@bb/host-daemon-contract --force` — passed.
- `pnpm exec turbo run test --filter=@bb/host-daemon-contract --force --
test/contract.test.ts` — 38 passed.
- The full host-daemon-contract suite was also run locally: 51 tests
passed and its unrelated fixed gzip-byte measurement test differed under
local Node 26.3.1/zlib (`payload-size.test.ts`); the protocol contract
itself passed.

Fixes get-bb#2023

> AGENT GENERATED: by GPT-5
## What was wrong

Long-lived threads repeatedly scanned JSON payloads for todo tool names,
rebuilt the full conversation outline for unrelated command and
reasoning events, and pruned arbitrarily large sets of resolved deltas
in one synchronous SQLite write. Those paths blocked the server event
loop and delayed otherwise small event inserts. Separately, stall
diagnostics attributed awaited RPC wall time as event-loop work, treated
laptop suspension as a runtime stall, and warned on fresh 512-event
bursts before there was evidence that delivery was stuck.

## What changed

- Add a guarded generated tool-name column and partial todo lookup index
through Drizzle migration 0104.
- Key the conversation-outline cache by the latest outline-relevant
event while still returning the current thread sequence.
- Use the materialized parent-tool-call column in remaining event
queries and cap each resolved-delta prune pass at 500 rows.
- Attribute event-loop stalls only to completed synchronous work; keep
awaited routes visible only as current work.
- Reset server and host event-loop samples after likely system
suspension and report the host heartbeat wake as informational.
- Require a depth-512 daemon event queue to remain queued for five
seconds before warning, while retaining the unconditional thirty-second
age warning.

The timeline byte limit and default event budget are intentionally
unchanged. There are no server/host wire changes, so
HOST_DAEMON_PROTOCOL_VERSION is unchanged.

## How you verified

- pnpm exec turbo run test --filter=@bb/db --force: 406 tests passed.
- pnpm exec turbo run test --filter=@bb/host-daemon --force: 586 tests
passed.
- Affected server suites: 91 current tests passed, including outline
caching, event-loop attribution, and timeline-window regression
coverage.
- pnpm exec turbo run test --filter=@bb/config --filter=@bb/domain
--force: 108 config and 137 domain tests passed.
- Affected app tests passed: 43 tests.
- pnpm exec turbo run typecheck for @bb/db, @bb/domain, @bb/config,
@bb/host-daemon, @bb/server, and @bb/app: all passed.
- Reproduced the todo lookup on a copied 41k-event thread: median
11.46ms to 0.04ms. Reproduced a 5k-delta prune: median 10.05ms to 1.22ms
per bounded pass.

The full server suite was also attempted, but existing npm-artifact
packaging tests do not produce a clean signal in this sandbox; all
suites covering changed server paths passed.

Fixes: N/A — log-driven performance investigation.

> AGENT GENERATED: by GPT-5
## What was wrong

Session-open validation required the newer `localApiPort` field before
comparing daemon and server protocol versions. Daemons from before that
field existed therefore received `400 invalid_request: Required` instead
of `protocol_version_mismatch`; because the daemon only invokes its
protocol self-updater for the latter response, an enrolled older daemon
could retry forever while the server reported it offline.

## What changed

- Default a missing `localApiPort` to `null` at the server boundary so
pre-field session payloads reach the protocol-version check.
- Keep the current daemon-side request type explicit by exporting the
schema's parsed output type.
- Add a regression request frozen to the pre-`localApiPort` wire shape,
which protects future required session fields from bypassing the
mismatch response.
- Bump `HOST_DAEMON_PROTOCOL_VERSION` from 142 to 143 for the
wire-boundary behavior change.

## How you verified

The new server regression reproduces the old daemon payload without
`localApiPort` and now receives `protocol_version_mismatch`; before the
fix, the live equivalent received `invalid_request: Required`.

- `pnpm exec turbo run test --filter=@bb/host-daemon-contract --force --
--run test/contract.test.ts`
- `pnpm exec turbo run test --filter=@bb/server --force -- --run
test/internal/internal-session-protocol-version.test.ts`
- `pnpm exec turbo run test --filter=@bb/host-daemon --force -- --run
src/server-client.test.ts src/protocol-self-update.test.ts`
- `pnpm exec turbo run test --filter=@bb/scripts --force -- --run
test/request-dev-restart.test.ts`
- `pnpm exec turbo run typecheck --filter=@bb/host-daemon-contract
--filter=@bb/host-daemon --filter=@bb/server`
- `pnpm exec prettier --check
packages/host-daemon-contract/src/session.ts
packages/host-daemon-contract/src/protocol.ts
packages/host-daemon-contract/test/contract.test.ts
apps/server/test/internal/internal-session-protocol-version.test.ts`
- `git diff --check`

Fixes: N/A (no linked issue).

> AGENT GENERATED: by GPT-5
## What was wrong

The active thread timeline and the full-history conversation outline
shared the same realtime invalidation group, so every events-appended
streaming batch sent another /conversation-outline request. The server
cache hardening now on main from get-bb#2025 avoids rebuilding for
outline-irrelevant events, but the client still performs redundant HTTP
reads at streaming cadence, and assistant text deltas can still
invalidate the full projection. The outline does not need sub-second
route refreshes because the incremental timeline already carries the
live conversation rows.

## What changed

- Split realtime timeline-window invalidation from conversation-outline
invalidation.
- Refresh the full outline at the terminal turn boundary instead of for
every streaming delta; unknown lifecycle notifications still invalidate
it conservatively, and history rewrites retain the existing full
invalidation path.
- Overlay live timeline conversation rows onto the cached full outline
so current user and assistant messages remain fresh while a turn
streams.
- Reconciled the server cache documentation with the outline-aware cache
added by get-bb#2025 and the new client refresh policy.
- Added regressions proving streaming deltas do not refetch the outline,
turn completion does, and live timeline labels replace or extend a
cached outline.

This implements the client-side pacing direction from get-bb#1972 using a turn
boundary plus live-row overlay instead of a timed debounce. It does not
change the API contract or the server/daemon wire format, so
HOST_DAEMON_PROTOCOL_VERSION is unchanged.

## How you verified

The new realtime invalidation test fails before the change because an
assistant delta refetches the active outline query. The TOC merge test
also fails before the change because a loaded outline always wins over
newer timeline rows.

After rebasing onto origin/main at 0b2723a:

- pnpm exec turbo run test --filter=@bb/app --
src/hooks/cache-owners/cache-owner-registry.test.ts
src/hooks/realtime-cache-effects.test.ts
src/components/thread/toc/ThreadTableOfContents.test.tsx — 80 tests
passed
- pnpm exec turbo run typecheck --filter=@bb/app — passed
- git diff --check origin/main...HEAD — passed

Fixes get-bb#1972

> AGENT GENERATED: by GPT-5
## What was wrong

The mobile thread header showed a "Working" subtitle with a spinner
under the title while a thread ran. The timeline already shows a working
indicator, so the header line was noise.

## What changed

- `apps/mobile/src/screens/thread/ThreadDetailHeader.tsx`:
`headerSubtitle` hides working-tone statuses (Working, Provisioning,
Starting, Stopping, Reconnecting). The header keeps "Needs input",
"Error", "Waiting for host", "Archived", and the child / side chat
label. The spinner is gone.
- `apps/mobile/src/screens/thread/thread-detail-header-model.ts`:
removed the unused `spinning` field from `ThreadStatusPill`.

## How you verified

- `pnpm exec turbo run typecheck --filter=@bb/mobile` passes.
- Manual check in the iOS simulator against the mobile e2e backend: an
active thread shows only the title in the header, and the timeline still
shows "Working...".

Fixes #

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong

The iOS unread divider used the `attention` token (yellow), a centered
label, and two rules. The web app uses `timeline-accent` (blue), a left
label, and one rule. The two apps did not match.

## What changed

`apps/mobile/src/screens/thread/timeline/TimelineList.tsx`: the divider
now uses `text-timeline-accent` / `bg-timeline-accent`, an uppercase
medium-weight "New" label on the left, and one rule on the right. This
matches `UnreadDivider` in
`apps/app/src/components/thread/timeline/ThreadTimelineRows.tsx`.

## How you verified

`pnpm exec turbo run typecheck --filter=@bb/mobile` passes. Visual
change only; no new tests.

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
…2034)

## What was wrong

The mobile composer's voice bar
(`apps/mobile/src/composer/VoiceBar.tsx`) showed a red dot, a
"Listening…" label, and an elapsed timer. It gave no live audio feedback
and did not look like the web `VoiceRecordingBar`, which draws scrolling
sound-wave bars from the microphone level.

## What changed

- `apps/mobile/src/composer/voice-waveform-model.ts` (new): pure port of
the web `WaveformVisualizer` math. `meteringToAmplitude` converts
expo-audio metering (dBFS) to a bar amplitude with the same noise floor,
gain, and gamma as the web RMS path; plus the scrolling bar buffer and
the SVG path builder.
- `apps/mobile/src/composer/VoiceWaveform.tsx` (new): draws the bars as
one `react-native-svg` path (3px bars, 2px gaps, round caps, newest at
the right, oldest fading on the left via a gradient stroke). Samples
`readLevel()` at ~30 Hz while active, freezes when inactive, shows flat
idle bars under reduce-motion.
- `apps/mobile/src/composer/VoiceBar.tsx`: web layout — round ghost
cancel · waveform · round primary confirm. While transcribing the bars
freeze and breathe (the `animate-shine-icon` stand-in) and the confirm
button shows a spinner.
- `apps/mobile/src/composer/useComposerVoice.ts`: records with
`isMeteringEnabled: true` and exposes `readLevel()`; the elapsed-seconds
ticker is removed.
- `apps/mobile/app/dev/ui.tsx`: a "Voice bar (synthetic levels)" gallery
section so the bar can be exercised without a mic.

No wire changes.

## How you verified

- New `voice-waveform-model.test.ts` (dB mapping floor/clamp/monotonic,
scroll buffer, path geometry). `pnpm exec turbo run test typecheck lint
--filter=@bb/mobile` pass.
- iOS Simulator (iPhone 17 Pro) through the dev client and the UI
gallery: recording scrolls right→left with the left-edge fade; Check →
transcribing freezes and breathes with a spinner; X → recording resumes.
Checked dark and light.

Fixes #

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong

The mobile app's left drawer (`expo-router/drawer`) repeated the home
screen: home already shows the grouped thread list with the compose
dock. The drawer only added the server switcher, Settings, display
options, and a path to another thread from inside a thread. It cost an
edge gesture and a scrim, and the bb connect edge-swipe press-through
(noted in the Phase 7 integration entry) came from it.

## What changed

- `apps/mobile/app/(drawer)/` and `src/screens/shell/DrawerContent.tsx`
are removed. `app/index.tsx` (home) is the root of the native stack;
`_layout.tsx` anchors on `index`.
- New `src/screens/shell/WorkspaceMenu.tsx`: the home header's left
button is the active server's initials with the realtime dot. It opens a
bottom sheet with the server label and connection state, the server
rows, Add server, Archived threads, Settings, and UI gallery in E2E
mode. It dims with the compose scrim.
- `HomeScreen.tsx` sets the title (server label) and the header-left
button in every ready state. Search and display options stay in the
header's right slot.
- Dead code removed: `SidebarActionsProvider.onBeforeNavigate`, and
`selected` on `SidebarThreadList` / `SidebarThreadRowView` (only the
drawer highlighted the open thread).
- E2E: new `e2e/subflows/open-settings.yaml` (avatar → Settings)
replaces every `drawer-*` step in 10 flows. `phase1-shell` asserts the
sheet contents, `phase3-threads` searches from `home-search`,
`phase5-connect` drops the header-toggle workaround.
- Docs: `apps/mobile/README.md`, a new entry in
`plans/bb-mobile-progress.md`.

No wire changes.

## How you verified

- `pnpm exec turbo run typecheck lint test --filter=@bb/mobile`: green,
817 tests (adds `workspace-initials.test.ts`).
- Maestro on the iPhone 17 Pro simulator (iOS 26.3) against the e2e
harness: `phase1-shell` and `phase3-threads` pass end to end.
Screenshots show the avatar in the header and the workspace sheet.
- `phase7-settings` was not run: its pre-flight
`phase7-settings-reset.js` got a 400 from the harness before the app
launched (unrelated to this change).

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
…-bb#2038)

## What was wrong

The mobile app had no EAS project, no store credentials, and no release
path. \`app.json\` had no \`extra.eas.projectId\`, \`eas.json\` had an
empty submit profile, and the nightly publish workflow built desktop
only. Nothing could reach TestFlight.

## What changed

- \`apps/mobile/app.json\`: link to the EAS project \`@bb-team/bb-app\`
(slug \`bb-app\`, owner \`bb-team\`, \`extra.eas.projectId\`). Set
\`ITSAppUsesNonExemptEncryption: false\` so each TestFlight build skips
the export-compliance question. The dev-client scheme is now
\`exp+bb-app://\` (e2e launch subflow and the incoming-link test
follow).
- \`apps/mobile/eas.json\`: \`submit.production\` with the Apple team,
App Store Connect app id \`6803559210\`, the API key id and issuer id,
and the key path \`./asc-api-key.p8\` (gitignored via \`*.p8\`).
- \`apps/mobile/package.json\`: pin \`eas-cli@22.0.0\` as a
devDependency so local and CI runs share one version (\`pnpm exec eas
…\`).
- \`.github/workflows/publish-bb-app.yml\`: new \`nightly-mobile-ios\`
job, gated like the desktop nightly jobs. On Ubuntu it writes the
numeric base of the nightly version into \`app.json\` (iOS rejects
prerelease strings; the remote EAS build number tells nightlies apart),
writes the \`.p8\` from the \`ASC_API_KEY_P8\` secret, and runs \`eas
build -p ios --profile production --non-interactive --no-wait
--auto-submit\` with \`EXPO_TOKEN\`. EAS builds on its own macOS workers
and uploads to TestFlight.
- \`apps/mobile/README.md\`: the Release section now documents the real
setup, the manual TestFlight path, the nightly job, and the two repo
secrets.

Out of band (not in the diff): EAS holds the iOS distribution
certificate, App Store provisioning profile, and APNs push key; the
\`EXPO_TOKEN\` (robot, Developer role on \`bb-team\`) and
\`ASC_API_KEY_P8\` repo secrets are set.

## How you verified

- \`eas build -p ios --profile production\` from this branch built green
on EAS (build 1246687c, version 0.0.1 build 2); the pnpm
\`expo-modules-jsi\` patch and \`lightningcss\` override applied on the
EAS image.
- \`eas submit -p ios --latest --non-interactive\` with the committed
submit profile uploaded to App Store Connect (submission b718dedb). The
App Store Connect API reports build 2 as \`processingState: VALID\`.
- \`eas whoami\` with the robot token authenticates as \`bb-team\`
(Developer).
- \`pnpm exec turbo run test --filter=@bb/mobile --force\`: 119 files,
813 tests pass.
- \`actionlint\` on the workflow: only the pre-existing Blacksmith
runner-label warnings.

Fixes #

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong

The mobile app still shipped the Expo template icon set: the blue
chevron app icon, the Expo adaptive icon layers, the Expo splash icon
and favicon, and the default `#E6F4FE` adaptive icon background and
notification color. The desktop app ships the black \`bb\` glyph on
white, so the two apps did not match on a home screen.

## What changed

- \`apps/mobile/assets/\`: regenerated \`icon.png\` (1024 opaque, white
background), \`android-icon-foreground.png\`,
\`android-icon-background.png\`, \`android-icon-monochrome.png\`,
\`splash-icon.png\`, and \`favicon.png\` from
\`apps/desktop/assets/icon.png\`.
- \`apps/mobile/app.json\`: the Android adaptive icon
\`backgroundColor\` is now \`#FFFFFF\` and the notification accent
\`color\` is \`#000000\`.

No wire changes, no CLI or doc changes.

## How you verified

- Built a Release app with \`expo run:ios --configuration Release
--no-bundler --device <udid>\` and installed it on an iPhone 15 Pro. The
home screen shows the \`bb\` glyph.
- Compared the new \`icon.png\` against the desktop asset by eye at
1024x1024.

Fixes #

> AGENT GENERATED: by Claude Opus 5

Co-authored-by: Claude <noreply@anthropic.com>
…b#2026)

## Human comments

Before this, (1) custom file openers did not work on files opened via
CMD+P, and (2) a custom file opener would render with nearly 0px height:
<img width="666" height="815" alt="Screenshot 2026-08-20 at 1 14 41 AM"
src="https://github.com/user-attachments/assets/54a8b8b5-f903-49ce-a940-e9f8bdac5c16"
/>


-----

## What was wrong

The `fileOpener` plugin slot could not work end to end — three
independent defects, each of which alone made the slot unusable.
**Reachability:** the secondary panel's file search built its tab
through `createTabForFileSearchSelection` and never called
`createFileOpenerTabForRequest`, so a file picked from the
"+"/quick-open screen always got the built-in preview regardless of the
user's Settings → File openers choice; diversion applied only to file
links and `bb thread open`, contradicting the comment on `openTab` that
claims every file-open flow funnels through it. **Sizing:**
`fileTabContentFillsRegion` resolved the active tab's `actionId` against
`threadPanelActions`, but a file-opener tab's actionId is
`file-opener:<id>` (`FILE_OPENER_ACTION_ID_PREFIX`) and never matched,
so opener tabs always landed in the preview's scroll container rather
than the definite-height region; and the file-opener wrapper was
`min-h-0 flex-1` where the action-tab wrapper 90 lines above it is
`h-full min-h-0 flex-1`. Since that region is a block box, `flex-1` was
inert and the wrapper collapsed to content height, so an opener that
sizes itself with `flex-1` rendered at zero height.

Found while building a Monaco-based editor plugin against the slot: the
opener registered, appeared in Settings, was explicitly selected for
`.ts` and `.json`, and still never rendered — and once forced to render,
occupied ~10px.

## What changed

- `apps/app/src/components/secondary-panel/useThreadFileTabs.ts` —
`selectFileSearchResult` now runs the same opener diversion as
`openTab`, falling back to the built-in tab when no opener matches. The
replace-the-new-tab-screen behavior is unchanged.
- `apps/app/src/views/thread-detail/ThreadDetailView.tsx`,
`apps/app/src/views/RootComposeView.tsx` — `fileTabContentFillsRegion`
now also returns true for file-opener tabs, keyed off `fileOpenerOwner`
(already set on exactly these tabs). A plugin opener owns its own layout
and scrolling, so it gets the definite-height region, matching a
`layout: "flush"` action tab.
- `apps/app/src/components/plugin/PluginPanelActions.tsx` — the
file-opener wrapper gains `h-full`, matching the action-tab wrapper.

No wire changes, so `HOST_DAEMON_PROTOCOL_VERSION` is untouched. No CLI,
guide, or doc surfaces are affected: this restores documented behavior
rather than adding any.

Both sizing changes are required. Without the region fix the opener
fills a scroll container and overflows by its `pb-3`; without `h-full`
the wrapper stays content-sized however tall the region is.

## How you verified

Three tests added to `useThreadFileTabs.test.ts`, alongside the existing
`openTab` diversion coverage:

- `diverts a workspace file picked from the file search` — **fails
before, passes after**. Verified by restoring the pre-fix
`useThreadFileTabs.ts` with the new tests in place: 17 pass, this one
fails; with the fix, 18 pass.
- `keeps the built-in preview for an unmatched file search extension`
and `honors a pinned built-in preference from the file search` — pass
both before and after. They are guards, not regression proofs: they pin
the fallbacks so a future change cannot start diverting files the user
asked BB to keep.

```
pnpm exec turbo run test --filter=@bb/app -- useThreadFileTabs   # 18 passed
pnpm exec turbo run typecheck --filter=@bb/app                   # clean
```

The two sizing defects are **not** covered by automated tests. They are
CSS-in-DOM-context failures — the wrapper collapses only because its
ancestor is a block box — and the only cheap unit test available would
assert a Tailwind class string, which is the kind of test AGENTS.md
discourages. Testing them for real would mean extracting the
`fileTabContentFillsRegion` computation out of both views into a helper;
happy to do that if reviewers want it covered.

Verified manually against a dev server on this checkout with a Monaco
`fileOpener` plugin installed. Before: quick-open a `.ts` file →
built-in preview, with the plugin's registration confirmed live in the
console and "Automatic (Monaco)" selected in Settings; pinning `.json`
to Monaco explicitly changed nothing. Walking the DOM from
`[data-testid="plugin-file-opener-tab-content"]` showed the wrapper at
`clientHeight: 38` inside a `display: block` scroll container at 775px.
After: quick-open renders the plugin editor, filling the panel, with
editing, saving, and find working.

Fixes #

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What was wrong

The builtin Provider retry plugin was explicitly registered with
`defaultEnabled: false`, so fresh installations shipped automatic
subscription-limit recovery disabled even though the plugin is bundled
and auto-installed.

## What changed

- Enable `provider-retry` when its builtin registration is first
installed while preserving the stored choice for existing installations.
- Add focused coverage for the fresh-install default and for preserving
an existing disabled choice.
- Require the plugin to reach `running` in the packaged-app smoke test.
- Update configuration docs, CLI guides, the bb-cli skill, and the QA
runbook to describe the new default.
- No server/host-daemon wire behavior changed;
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- `pnpm exec turbo run typecheck --filter=@bb/server`
- `pnpm exec turbo run test --filter=@bb/server --
test/services/plugins/builtin-plugins.test.ts -t 'Provider retry
enabled|preserves an installed builtin'` (2 passed)
- Prettier check across all changed files
- The full builtin-plugin test file also passed the 22 unaffected/new
cases; two existing source-watcher cases hit the local sandbox's
`EMFILE: too many open files, watch` limit.

Fixes: no linked issue.

> AGENT GENERATED: by GPT-5
## What was wrong

Production-bundle QA required choosing between the convenient worktree
isolation of `pnpm dev` and the production build/serving behavior of
`pnpm start`. The dev launcher already derived stable checkout-specific
data and ports, but only launched the Vite development server; the
production launcher used the desired optimized, same-origin bundle path
without applying those worktree selectors.

## What changed

- Added `pnpm start:worktree`, which reuses the development dotenv
cascade and checkout-specific data/server/host-daemon selectors before
invoking the existing production-style source launcher.
- Added a typed worktree runtime policy that is reapplied after
persisted `config.json`/`env.json` settings are loaded, locking the
worktree data directory, ports, inherited skills, listener host, absent
Vite port, and disabled telemetry.
- Made `start-bb.mjs` build children lead process groups and forward
SIGINT/SIGTERM with leader-first shutdown and escalation, waiting until
descendant processes are gone.
- Added focused launcher-policy and real process-tree SIGTERM tests.
- Documented the command in the README, configuration/debugging guides,
and platform support list.
- No server/host-daemon wire contract changed, so
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- `pnpm exec turbo run typecheck --filter=@bb/scripts --filter=bb-app`
- `pnpm exec turbo run test --filter=@bb/scripts --filter=bb-app
--force` (`@bb/scripts`: 18 files/97 tests; `bb-app`: 1 file/65 tests)
- `pnpm exec prettier scripts/start-bb.mjs
packages/bb-app/src/launcher.ts packages/bb-app/src/index.ts
packages/bb-app/test/index.test.ts
packages/scripts/src/commands/run-dev.ts
packages/scripts/test/run-dev.test.ts
packages/scripts/test/start-bb.test.mjs docs/configuration.md
docs/platform-support.md --check`
- The process regression sends SIGTERM to a live launcher fixture and
asserts both its build leader and grandchild are gone before exit.
- Started `BB_TELEMETRY=false pnpm start:worktree`, fetched the worktree
server URL, and confirmed it returned hashed `/assets/*.js` production
bundles with no Vite client or source-module entry. Ctrl-C stopped both
listeners.

Fixes get-bb#2044

> AGENT GENERATED: by GPT-5
…2043)

## What was wrong

`POST /threads/:id/queued-messages` (`createQueuedMessageForThread` in
`apps/server/src/routes/threads/actions.ts`) only checked
archived/stopping/deleted. It never applied the gone-environment rule
(`goneThreadEnvironmentDetails`) that the direct send path applies
through `requireThreadCommandEnvironment`. A thread whose managed
worktree was destroyed (archive → grace window → destroy → unarchive)
kept `status: idle`, answered `201` with a queued-message id, and the
message could never drain: the auto-send hit the same `409
thread_environment_unavailable` internally and the 10 s sweep retried
forever. The CLI also labelled the destroyed worktree "Provisioning".

Issue: get-bb#1789. Report: https://get-bb.github.io/reports/issues/1789.html

## What changed

- `apps/server/src/routes/threads/actions.ts`:
`createQueuedMessageForThread` admits the message inside the same `BEGIN
IMMEDIATE` transaction as the insert, on the freshly loaded thread row
(`admitQueuedMessage`): writable check, environment check, and one
provider-thread-id read. A `destroying`/`destroyed` environment returns
the same `409 thread_environment_unavailable` as the send path. A thread
with `environmentId === null` that already has a provider thread id (the
row was pruned after destroy) returns `409` with reason
`never_attached`, again the same as send. A thread that has not run yet
and has no environment still accepts queued messages, because that is
how messages wait for provisioning.
- `packages/db/src/data/queued-thread-messages.ts`:
`createQueuedThreadMessageInTransaction` for caller-owned transactions;
`listIdleThreadsWithQueuedMessages` joins `environments` and skips
`destroying`/`destroyed`, so queued rows that survived archive → destroy
→ unarchive no longer fail the sweep every 10 s.
- `packages/core-ui/src/environment-display.ts`:
`formatEnvironmentDisplay` labels a `destroying` environment
"Destroying" and a `destroyed` one "Destroyed" instead of
"Provisioning". This changes `bb thread show` and app metadata labels.
- No wire shape changed, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged.
No new routes or CLI flags.

Not done here: the report also suggests a
`runtime.environmentStatus`/`canRun` field on the thread response. That
is an API contract addition and is left as a follow-up.

## How you verified

- `apps/server/test/public/public-thread-queue-gone-environment.test.ts`
(from the report, extended): 3 of 5 tests fail before the route fix
(`expected 201 to be 409`); the sweep test (real `archiveThread` →
`retire.requested` → `destroy.started` → `destroy.completed` → `POST
/unarchive`) fails before the query change; all 5 pass after. One test
is a control that a never-run, environment-less thread still gets `201`.
- `packages/core-ui/test/environment-display.test.ts`: new case for
`destroying`/`destroyed` labels.
- `pnpm exec turbo run test typecheck --filter=@bb/db
--filter=@bb/server --filter=@bb/core-ui --filter=@bb/cli`: typecheck
clean; db 406/406; core-ui 17/17; cli 452/452; server 1798/1799. The one
failure is `test/internal/internal-skill-trees.test.ts` (file mode `420`
vs `436`), a umask difference on this machine, unrelated to this change.

Fixes get-bb#1789

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
…2042)

## What was wrong

A thread can never be archived once its environment row is gone.
`pruneDestroyedEnvironments` hard-deletes `destroyed` environment rows
after 7 days with no live-thread guard. `threads.environment_id` is `ON
DELETE SET NULL`, so a still-unarchived thread silently loses its
pointer. `POST /threads/:id/archive` and `/archive-all` then call
`requireThreadHostCommandEnvironment`, which throws `409
thread_environment_unavailable` (`never_attached`). The app shows
"Workspace is not available yet." and the thread stays in the sidebar.
Delete was the only way out.

The archive path only uses the environment for
`requestActiveRuntimeThreadStopIfNeeded`, which is a no-op for an idle
thread with no environment. The requirement is not load-bearing.

Report: https://get-bb.github.io/reports/issues/1924.html

## What changed

Server only. No wire shape change, so no `HOST_DAEMON_PROTOCOL_VERSION`
bump.

- `thread-command-environment.ts`: add
`resolveThreadHostCommandEnvironment`. It returns `null` for a `null`
pointer and still throws for a dangling non-null id.
- `thread-archive.ts`:
`ArchiveThreadWithLifecycleEffectsArgs.environment` is nullable. Skip
the runtime stop when `null`. Hidden forks and
`archiveThreadAndChildren` use the resolver; only non-null environments
enter `affectedEnvironmentIds`.
- `routes/threads/actions.ts`: `routes.archive` uses the resolver.
`routes.stop` uses the resolver in place of its inline null branch (same
behavior).

Not changed: the prune sweep still removes rows that live threads point
at. That is a separate behavior decision; this PR makes archive tolerate
the state.

## How I verified

- New test `apps/server/test/threads/archive-pruned-environment.test.ts`
seeds a thread, marks its environment destroyed 8 days ago, runs
`pruneDestroyedEnvironments`, and archives via `/archive` and
`/archive-all`. Both cases fail with 409 before this change and pass
after.
- `pnpm exec turbo run test typecheck --filter=@bb/server`: typecheck
clean; 1795/1796 tests pass. The one failure is
`internal-skill-trees.test.ts` (file mode 420 vs 436, umask) and fails
identically without this change.

Fixes get-bb#1924

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
andrewkchan and others added 29 commits August 25, 2026 11:29
## What was wrong

The plugin store had no popularity signal. Every listing looked equally
used, so nothing distinguished a widely adopted plugin from one nobody
installs. The data already existed — servers have sent a
`plugin_installed` telemetry event
(`apps/server/src/services/system/telemetry.ts`) since the store
shipped, carrying a `plugin_id` for bundled plugins and `bb-community`
entries — but nothing ever read it back.

The curated marketplace now publishes those counts as a `stats.json`
sidecar beside its manifest (get-bb/marketplace#98). This PR consumes
it.

## What changed

Six commits, each typechecking on its own and meant to be read in order.

**1. `db: add a stats_json column to plugin_marketplaces`**
The column, nullable, plus migration `0107`. `statsJson` is required on
`UpsertPluginMarketplaceInput` rather than defaulted, so a refresh that
did not re-read the sidecar keeps its counts by explicitly passing the
value it had. All three writers are updated to say which they mean, and
nothing fetches a sidecar yet — **no behavior change**. The migrate test
drops the column before replaying a rewind, matching how every other
`ALTER TABLE ADD` in that suite is handled.

**2. `db: record the drizzle snapshot for 0107`**
**Skippable** — 3,739 lines of generated JSON, split out so it does not
bury the rest. It has to be committed even though nothing reads it at
runtime: drizzle-kit generates each migration by diffing `schema.ts`
against the newest snapshot in `meta/`. Without it, the next person to
run `db:generate` diffs against 0106, does not see `stats_json`, and
re-emits the same ALTER inside their own migration — which then fails on
every database that already applied 0107. All 109 snapshots are tracked
for this reason.

**3. `server: parse and fetch the marketplace install-count sidecar`**
A self-contained module, unused until commit 4. Three decisions live
here: the fetch is unconditional rather than replaying the manifest's
ETag, because the counts move while the manifest sits unchanged behind a
304 — that is the whole reason they are a separate document rather than
a manifest field, which the strict manifest schema would reject on an
older desktop anyway. A missing sidecar answers null. And the schema is
deliberately *not* strict, unlike the manifest's: that one is a security
contract where an unknown field must reject the document, while this is
display metadata a later publisher may extend. A malformed document is
still rejected whole, because half-parsed counts are worse than none.

**4. `server: read install counts on refresh and report them in
search`**
The wiring, and the only commit that changes what a client sees. Counts
are stored in the same transaction as the catalog snapshot, so entries
and counts always publish together; a failed fetch warns and keeps the
stored counts, since a cosmetic number must never fail a catalog
refresh. **Only the curated marketplace is asked for a sidecar** — BB
measures these from its own telemetry, so a number beside a third-party
listing would be that publisher's claim wearing BB's label. Bundled
plugins do get counts, from the same document. `installs` joins the
search result as nullable with a null default, so an older server
degrades to *no count* rather than to zero: zero would be a claim, null
is the absence of one.

**5. `app, mobile, cli: show install counts`**
Store card footer (compact `4.2K installs`, exact number in the
`title`), mobile browse subtitle, and an `Installs` column in `bb plugin
search` — exact and comma-grouped there, since a terminal column is read
to be compared, and present only once some result carries a count. An
uncounted entry renders nothing at all rather than a zero, on every
surface.

**6. `docs: document the install-count sidecar`**
Plan doc gets the format and the reasoning; the guide chapter and bb-cli
skill get the user-facing half, since `bb plugin search` grew a column.

No `HOST_DAEMON_PROTOCOL_VERSION` bump: nothing here crosses the
server/daemon boundary. `apps/web` needed no change — the R2 route
already serves any key under `/marketplace/v1/` and gives `.json` the
revalidating cache-control.

**Caveat worth stating in review:** telemetry is opt-out and only
production builds report, so the number is installs BB heard about, not
a true total. The docs say so; the UI just shows the number.

## How you verified

Six new server tests, all failing before commit 4:
- counts land on both curated entries and bundled plugins
- an entry the sidecar does not name stays uncounted (not zero)
- **the sidecar is re-read while the manifest answers 304** — the core
reason for the split
- a failed sidecar fetch keeps the stored counts and the refresh still
records success
- a malformed document (negative count) is rejected whole
- a third-party marketplace's entries stay null and its `stats.json` is
never requested

Plus a store-card test (compact label, exact `title`, singular "1
install", no count for the third-party card, footer does not collapse
into a stray separator) and a CLI test (column absent without counts,
present with them).

Commands: `pnpm exec turbo run typecheck` clean across all 75 tasks **at
each of the six commits individually**. `test` green for `@bb/server`
(1928), `@bb/db` (406), `@bb/cli` (463), `@bb/mobile` (839),
`@bb/server-contract` (58), `@bb/app`.

Also fed the registry's real generated output through
`parseMarketplaceStatsJson` in a scratch test, to confirm the two repos
agree on the format.

Pre-existing failures, unrelated and present on a clean tree:
`PluginIcon.test.tsx` (a local untracked `plugins/` directory without a
`package.json`) and occasional 5s timeouts in `FilePreview` /
`update-resolver` / `timeline-in-turn-window` under concurrent
full-suite load — each passes in isolation.

Fixes #

> AGENT GENERATED

---------

Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
## What was wrong

The physical-workspace checkout guard treated every non-deleted `idle`
thread as a live workspace user, even after the thread was archived.
Because archiving preserves `idle` status, a project could archive all
of its threads and still be unable to create a new branch-backed thread
in that workspace.

## What changed

The host-path query now applies the existing live-thread predicate,
which excludes archived and deleted rows, while retaining the
`starting`, `idle`, and `active` status check that protects workspaces
used by unarchived threads. The guard remains host/path-wide across
projects and applies only when the request checks out a branch.

Focused in-memory SQLite coverage now verifies archived and deleted
release, unarchive reclaim, all protected statuses, hidden threads,
cross-project physical paths, and branch-checkout-only behavior.

## How you verified

- The deterministic in-memory harness reproduced the archived-idle
refusal before the fix and returned no refusal afterward.
- `pnpm exec turbo run test --filter=@bb/server --
test/threads/workspace-path-claims.test.ts` — 1 file, 7 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/db --filter=@bb/server` —
5 Turbo tasks succeeded.
- Actual web workflow in a disposable unmanaged Git project:
- archived-only holder: branch-backed creation changed from HTTP 409 to
HTTP 201;
- unarchived idle holder: branch-backed creation still returned HTTP
409.

Fixes get-bb#2068

> AGENT GENERATED

---------

Co-authored-by: Michael Yong <wrong92@gmail.com>
## What was wrong

The shared reserved-name inventory introduced for plugin CLI collisions
in get-bb#2411 covered only 10 of BB's 17 live core command groups plus
Commander's built-in `help`. Its regression test manually assembled the
same older subset instead of comparing with the authoritative
command-group registry, so the inventory could drift while the guard
stayed green. As a result, `bb plugin new settings` (and six sibling
names) created plugins whose short command was already core-owned, while
warning and discovery surfaces advertised that unreachable short form.

## What changed

Added the seven missing live core names: `settings`, `machine`,
`updates`, `terminal`, `file`, `marketplace`, and `voice`. Replaced the
duplicated hand-built test program with one exact invariant comparing
`RESERVED_BB_CLI_COMMANDS` against `CORE_COMMAND_GROUPS` plus `help`;
the existing command-group test independently proves that registry
matches every real top-level Commander registrar and has no aliases.

This preserves intentional plugin-plugin arbitration and keeps
plugin-contributed names such as `automation` and `connect` unreserved.
It adds no registry, manager, public API, CLI command/flag,
configuration knob, or documentation surface. No server/daemon wire
contract changed, so `HOST_DAEMON_PROTOCOL_VERSION` remains unchanged.

## How you verified

Before the production fix, the new focused invariant failed with exactly
the seven missing names. A detached build of the pre-fix `origin/main`
SHA reproduced `bb plugin new settings` exiting 0 and creating the
scaffold while `bb settings` remained core-owned; the fixed artifact
exits 1 with the reserved-name error.

After the fix:

- Focused CLI/server/SDK/app collision and discovery coverage: 7 files,
169 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/domain --filter=@bb/cli`
passed (5 tasks).
- `pnpm exec turbo run build --filter=@bb/cli` passed (4 tasks).
- `pnpm exec turbo run test --filter=@bb/domain --filter=@bb/cli
--force` passed: 79 files, 667 tests.
- `git diff --check origin/main...HEAD` passed.

Follow-up to get-bb#2411.

> AGENT GENERATED
## Context

Thread provisioning intermittently failed with
`thread_provisioning_failed` after waiting the full 30-second host RPC
timeout for a retryable inspection command. The daemon connection
remained usable, so repeating the inspection was safer and more useful
than failing the whole provisioning attempt.

This is transport resilience, not an OpenRouter/quota change.

## What changed

- Retry a host RPC once when its first response times out, but only
through `callHostRetryableOnlineRpc`.
- Preserve single-attempt behavior for ordinary/non-retryable host RPCs.
- Keep the existing reconnect wait for websocket-unavailable failures.
- Split the caller's existing timeout budget across two response-timeout
attempts, so enabling the retry does not double an established command
deadline.
- Increment `HOST_DAEMON_PROTOCOL_VERSION` to 167 because the server can
now send a second `host-rpc.request` after a response timeout.
- Add deterministic coverage where the first response is dropped and the
second request succeeds, plus deadline, non-retry, fresh-request-ID, and
late-response handling.

## Safety boundary

A timeout is ambiguous: the host may have executed the first request and
lost only its response. Retrying can therefore execute the command
twice. The retry helper accepts only
`HostDaemonRetryableOnlineRpcCommand`, derived from command-registry
entries explicitly marked `retryable: true`; commands not explicitly
safe to repeat cannot enter this path through the typed API.

Each attempt uses a fresh request ID. The first waiter's timeout removes
it from the hub, so a late response from the first attempt is treated as
stale and cannot resolve the second attempt. Tests also keep the
existing reconnect retry and ordinary-call single-attempt behavior
covered.

## Verification

- `pnpm exec turbo run test --filter=@bb/server --
test/hosts/online-rpc.test.ts test/system/execution-options.test.ts
test/public/public-provider-installations.test.ts` — 50/50 passed
- `pnpm exec turbo run test --filter=@bb/host-daemon-contract --
test/contract.test.ts` — 37/37 passed
- `pnpm exec turbo run typecheck --filter=@bb/host-daemon-contract
--filter=@bb/host-daemon --filter=@bb/server` — 6/6 Turbo tasks
successful
- `git diff --check origin/main...HEAD` — passed

The deterministic regression drops the first response while leaving the
daemon socket registered. Before this change the call rejected with `504
command_timeout`; after the change the second request succeeds inside
the original command budget. Fake timers pin the attempt boundary, and
the test verifies that the first late response is stale and the two
requests have distinct IDs.

---------

Co-authored-by: Michael Yong <wrong92@gmail.com>
## What was wrong

Plugin install telemetry and the trusted marketplace stats sidecar key
bundled plugins by canonical plugin ID, but the server's bundled
search-result mapper joined those counts with the separate source entry
name. Docs is the existing alias: its source entry is `docs`, while its
canonical plugin ID is `simple-notes`, so both the web store and `bb
plugin search` omitted its published count. The same mismatch would
affect any future bundled alias. This is a focused follow-up to get-bb#2282.

## What changed

The bundled mapper now reads the trusted count map with the canonical
`entry.pluginId`. The existing server regression uses the repository's
real `docs` / `simple-notes` registration, so it protects the class-wide
identity invariant rather than a same-name fixture.

Curated marketplace entries remain keyed by `entry.id`; third-party
counts remain null; lifetime-count and privacy semantics are unchanged.
No alias registry, compatibility layer, migration, persisted state,
public API, CLI/app code, guide/docs, or mobile change was added.
Nothing crosses the server/host-daemon boundary, so
`HOST_DAEMON_PROTOCOL_VERSION` remains 167.

## How you verified

- The regression failed before the production change with `expected null
to be 12`, then passed after it.
- Focused install-count group: 6 passed.
- `pnpm exec turbo run test --filter=@bb/server --force`: 211 test files
passed, 1 skipped; 2,030 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/server`: passed.
- `pnpm exec turbo run build --filter=@bb/server`: passed.
- Current dev API returned `{ entryId: "docs", pluginId: "simple-notes",
installs: 139 }`.
- Checkout-built `bb plugin search Docs` printed an `Installs` column
with 139.
- DevBrowser verified Extensions → Plugins renders `139 installs` on the
Docs card.
- `git diff --check`: passed.

Fixes: none — follow-up to get-bb#2282.

> AGENT GENERATED
## What was wrong

Side-chat forks persisted the selected-message reply anchor as
agent-only input, but deliberately started the native provider fork with
empty input so it remained idle. The first real send built its provider
command only from the new message, leaving the anchor out of the first
provider turn. This affected Codex, Claude Code, and Pi before
provider-specific translation. See get-bb#2316.

## What changed

- Resolve deferred agent-only thread-start context before the first real
provider-bound turn.
- Prepend that context to flat provider input and the first grouped
input for direct and queued sends.
- Revalidate eligibility inside the send transaction so concurrent sends
cannot deliver the anchor twice.
- Keep context deferred across a dispatch failure until a provider
`turn/started` exists; later turns never receive it again.
- Exclude agent-only context from user-editable prompt history while
retaining it on the accepted turn request.
- Always create reply context for a non-empty side-chat anchor,
including the latest source message, and remove the unnecessary
source-timeline lookup.
- Keep the initial native fork idle with an empty `thread.start.input`.
- Increment `HOST_DAEMON_PROTOCOL_VERSION` to 168 because
`turn.submit.input` semantics change. The wire shape and database schema
are unchanged.

## How you verified

The deterministic first-turn assertion failed on `main` before the fix:
expected `[seed, firstReply]`, received only `[firstReply]`.

- `pnpm exec turbo run test --filter=@bb/server --
test/public/public-thread-fork.test.ts
test/services/prompt-history.test.ts` — 29 tests passed.
- `pnpm exec turbo run test --filter=bb-plugin-side-chat` — 25 tests
passed.
- `pnpm exec turbo run test --filter=@bb/host-daemon-contract --
test/contract.test.ts` — 37 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/server
--filter=bb-plugin-side-chat --filter=@bb/db
--filter=@bb/host-daemon-contract` — 8 Turbo tasks passed.
- Browser flow with a side chat anchored to a BANANA instruction: before
the fix the provider answered APPLE; after the fix it answered BANANA.

Fixes get-bb#2316

> AGENT GENERATED

---------

Co-authored-by: Michael Yong <wrong92@gmail.com>
## What was wrong

Cursor supports a parameterized ACP model picker, but bb did not
advertise the client capability. Cursor therefore entered compatibility
mode: ACP sessions returned combined variant IDs while bb built the
picker from separate Cursor CLI model IDs. The split contracts could
omit Grok reasoning levels, and selecting bb's Default tier did not
explicitly clear Cursor's persisted Fast preference.

## What changed

- Advertise `clientCapabilities._meta.parameterizedModelPicker: true`
during both ACP model discovery and live start/resume/fork sessions.
- Discover and select bare ACP model IDs, then apply reasoning through
the advertised `effort` option.
- Send `fast=false` for Default and `fast=true` for Fast. If Cursor
advertises Fast but rejects the requested write, fail session
construction instead of silently retaining stale provider state.
- Probe Grok 4.6 and 4.5 first within the existing bounded discovery
deadline while preserving agent catalog order.
- Keep picker organization separate from probe priority: six bare Cursor
IDs remain in the primary menu and the rest populate
`selectedOnlyModels` under **More models**.
- Preserve existing CLI-backed ACP providers' `modelCli.primaryModels`
behavior and leave agents without a Fast option unchanged.
- Update the current provider-plugin/ACP-bridge architecture and bump
`HOST_DAEMON_PROTOCOL_VERSION` to `169` relative to current `main`.

## How you verified

- Added deterministic fake Cursor traffic covering compatibility vs
parameterized discovery, discovery/live capability placement, bare IDs,
effort mapping, explicit `fast=false`/`fast=true`, rejected Fast writes,
unsupported Fast providers, Grok-first probing, legacy CLI catalogs, and
primary vs selected-only model grouping.
- `pnpm exec turbo run test --filter=@bb/provider-bridge-acp
--filter=bb-plugin-provider-acp --filter=@bb/host-daemon-contract
--only`
  - ACP bridge/catalog/conformance: 284 tests passed.
  - ACP provider plugin: 76 tests passed.
  - Host-daemon contract: 51 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/provider-bridge-acp
--filter=bb-plugin-provider-acp --filter=@bb/host-daemon-contract`: all
six Turbo tasks passed.
- Verified eight real Cursor session constructions across Grok 4.5/4.6,
medium/high effort, and Default/Fast. All eight provider responses
persisted the requested bare model, effort, and Fast value before
prompting.
- Browser QA confirmed the curated six-model primary menu and collapsed
**More models** submenu.

## Notes

This change is separate from get-bb#1612, which tracks recursive workflow tool
schemas. The PR remains draft for maintainer review.

> AGENT GENERATED

---------

Co-authored-by: Michael Yong <wrong92@gmail.com>
## What was wrong

The prompt mention trigger scanner treated every space as the end of an
`@` query. Thread matching already supported multiword titles, but the
editor stopped dispatching the query at the first space, so those
matches were unreachable.

Extending a dismissed multiword occurrence could also reopen
autocomplete because dismissal was tied to the original text range.
Touch layouts had no explicit way to dismiss results while preserving
the typed text, and deleting then retyping a dismissed occurrence could
leave the new occurrence suppressed.

## What changed

Mention queries now preserve ordinary spaces verbatim. Tabs, newlines,
punctuation handling, the initially highlighted result, Enter, Tab,
matching, and ranking retain their previous behavior.

Dismissal now follows the same trigger occurrence as its text grows.
Removing or replacing that occurrence clears the dismissal so a newly
typed `@` can trigger autocomplete again.

Coarse-pointer layouts get a 44×44 `Close suggestions` target. It shares
the first section/status row rather than adding a separate header,
preserving the touch target without extra vertical space. Desktop menus
remain unchanged.

No wire contracts or protocol versions changed.

## How you verified

```bash
pnpm exec turbo run test --filter=@bb/client-core -- --run test/find-active-trigger.test.ts
pnpm exec turbo run test --filter=@bb/app -- --run src/components/promptbox/PromptBoxInternal.test.tsx
pnpm exec turbo run typecheck --filter=@bb/client-core --filter=@bb/app
pnpm exec turbo run lint --filter=@bb/app
pnpm exec oxfmt --check apps/app/src/components/promptbox/PromptBoxInternal.test.tsx apps/app/src/components/promptbox/PromptBoxInternal.tsx apps/app/src/components/promptbox/mentions/MentionMenu.tsx packages/client-core/src/prompt/mentions/find-active-trigger.ts packages/client-core/test/find-active-trigger.test.ts
git diff --check
```

- Client-core trigger scanner: 12 tests passed.
- PromptBox: 108 tests passed.
- App and client-core typechecks passed.
- App lint reported 181 pre-existing warnings and zero errors.
- Browser QA confirmed multiword lookup, unchanged Enter application,
persistent Escape/touch dismissal while typing, 44×44 touch close
sizing, and successful retrigger after deleting and retyping the
occurrence.

Fixes: N/A (no linked issue).

> AGENT GENERATED

---------

Co-authored-by: Michael Yong <wrong92@gmail.com>
…-bb#2422)

## What was wrong

The ask-user-question UI tests repeatedly used named `getByRole` queries
for ordinary click targets, even though these tests assert selection
state, payload construction, and callback invocation rather than
accessible-name computation. Testing Library recomputes accessible names
and visibility across every matching role candidate for each such query.
Under package-shard CPU oversubscription, that avoidable work dominated
the synchronous test body and exhausted Vitest's unchanged 5-second
ceiling. A focused 768-way contention reproduction produced the exact CI
signature at `app.test.tsx:75` in 6.765s; the full package reproduction
reported it in 8.548s with the same 1 failed/2 passed files and 1
failed/35 passed tests. Instrumentation separated a named option-role
query from its click at 512-way contention: the query took 2.163s while
the React click/update took 32ms. The verified origin/main and
merge-base for this investigation is
`c65d2ec8bf4be50ef8518fc026b85260cae73921`. CI evidence: [run
32892243548](https://github.com/get-bb/bb/actions/runs/32892243548),
[packages job
97946623016](https://github.com/get-bb/bb/actions/runs/32892243548/job/97946623016).

## What changed

`plugins/ask-user-question/app.test.tsx` now resolves visible labels to
their native `HTMLButtonElement` boundary through one small helper,
avoiding repeated accessibility-tree transforms while still proving that
each clicked label is rendered inside a real button. Submit and cancel
spies are asserted directly because those callbacks are invoked
synchronously by the click handlers; elapsed-time polling did not
represent a real lifecycle boundary. All payload, disabled-state,
preview, `aria-pressed`, navigation, cancellation, and malformed-payload
assertions remain intact. There are no production, wire-protocol,
timeout, CLI, or documentation changes.

## How you verified

- Before: `pnpm exec turbo run test --filter=bb-plugin-ask-user-question
--force` under 768-way contention reproduced the exact failure at
8.548s. The focused named test also reproduced at 6.765s.
- Intermediate structural check: after optimizing only the original
test, it passed in 2.070s but the same full stress run moved the timeout
to the sibling role-heavy test at 5.574s. This records package-shard
oversubscription as systemic amplification and justified applying the
same structural pattern across the file rather than distributing larger
clocks.
- After: the full unchanged-timeout package suite passed all 36 tests
under the same 768-way contention; `app.test.tsx` completed in 4.255s
and the original test in 1.393s.
- The exact named regression passed 20 consecutive Turbo-filtered runs.
- `pnpm exec turbo run test --filter=bb-plugin-ask-user-question
--force` — 36/36 tests pass unloaded.
- `pnpm exec turbo run typecheck --filter=bb-plugin-ask-user-question
--force` — passes.
- `pnpm exec turbo run build --filter=bb-plugin-ask-user-question
--force` — build graph passes (2/2 upstream generation tasks; this
private plugin has no package build script).
- `pnpm exec prettier --check plugins/ask-user-question/app.test.tsx` —
passes.

## Alternatives rejected

A timeout increase was rejected because the work is reducible and the
existing completion signals are synchronous; changing the clock would
hide the repeated transforms and weaken hang detection. Quarantining,
retrying, or disabling the test was rejected because the assertions
remain valuable. Changing production code was rejected because the
measured cost was in test queries, not the plugin interaction lifecycle.
Limiting the fix to the one recorded signature was also rejected after
the identical contention condition exposed the same structural issue in
a sibling test.

> AGENT GENERATED: by GPT-5.6-Sol
<!-- Keep the four sections. Delete this comment. See
docs/filing-issues.md for the issue side. -->

## What was wrong

There is no section for humans to add comments in their PRs.

## What changed

Add a section in the PR template.

## How you verified

See the PRs opened after this merges

<!-- Agents: end with the line below. -->
<!-- > AGENT GENERATED -->
## What was wrong

The host daemon periodically re-resolves the user shell PATH by trying
an interactive login shell and then falling back to a plain login shell.
A transient timeout or failure during a refresh could replace a
previously successful interactive PATH with the fallback PATH. On hosts
using a Node version manager, those paths can resolve different provider
executables and npm prefixes, so the UI could advertise an update for
one Pi installation while action preflight inspected another and
rejected the stale action.

## What changed

Added a stateful user-shell PATH resolver that retains the last
successful interactive PATH when a later interactive probe fails or
produces invalid output. Initial discovery still uses the existing
plain-login fallback when no previous PATH is available. Host daemon
startup now keeps one resolver for its runtime-shell refresh lifecycle.

No wire contracts changed, so `HOST_DAEMON_PROTOCOL_VERSION` is
unchanged.

## How you verified

- Added a regression test that fails before the fix and passes after it.
- `pnpm exec turbo run typecheck --filter=@bb/host-daemon`
- `pnpm exec turbo run test --filter=@bb/host-daemon --force --
src/runtime-shell-env.test.ts` — 17 passed.
- Full `@bb/host-daemon` suite — 559 passed.
- Live Pi status remained on NVM Pi `0.84.3` with no update action
across three refresh intervals.

Fixes: N/A — reported through a bb thread.

> AGENT GENERATED
## What was wrong

When loading older timeline rows, `BottomAnchoredScrollBody` captured
both the current `scrollHeight` and `scrollTop` before starting the
request. If the user continued scrolling while that request was in
flight, the pending anchor kept the earlier `scrollTop`. Once the rows
mounted, prepend compensation restored that stale position plus the
height delta, visibly undoing part of the user's scroll. Browser-native
scroll anchoring also emits scroll events during a prepend, so those
events cannot safely be treated as fresh user intent.

## What changed

- Track whether the next scroll event follows direct wheel, touch,
keyboard, or pointer input.
- While a prepend anchor is pending, advance its `scrollTop` to the
user's latest position but retain the original `scrollHeight` baseline
used for compensation.
- Clear the direct-input marker when capturing an anchor, so
browser-generated scroll events cannot replace the explicit position.
- Extend the scroll-preservation tests to exercise actual row prepends,
continued user scrolling during the load, and native browser anchoring.

This is UI-only and does not change the host-daemon protocol or any CLI
surface.

## How you verified

- Confirmed the new regression test failed before the implementation
(`expected 200`, `received 250`) and passed afterward.
- `pnpm exec turbo run test --filter=@bb/app -- --run
src/components/ui/bottom-anchored-scroll-body.scroll-preservation.test.tsx
src/components/thread/timeline/useAutoLoadOlderRows.test.tsx
src/components/thread/timeline/ThreadTimelineRows.height.test.tsx`
- `pnpm exec turbo run typecheck --filter=@bb/app`
- `pnpm exec turbo run test --filter=@bb/app --force` — 431 files and
3,358 tests passed; 3 skipped.
- `pnpm exec turbo run lint --filter=@bb/app` — 0 errors.
- Reproduced against the affected real thread with dev-browser and a
controlled 1.2-second older-page delay: the tracked message moved 252px
before the fix and 1px after it.

Fixes the timeline scroll-position jump while older messages load.

> AGENT GENERATED
## Human comments

<!-- Agents: Do not fill in this section. This is for human users to
fill in. -->

## What was wrong

Side-chat hidden reply context was inferred from the latest accepted
request and the absence of a later `turn/started`, instead of being
owned by the specific first accepted user turn. A rapid second send
accepted before provider start could therefore receive the anchor again,
while editing the completed owning turn rebuilt provider input without
the anchor.

## What changed

- Treat the accepted anchor-bearing request as the owner while its
thread is active, and allow retry only after the existing thread
lifecycle reports an observable dispatch error.
- Preserve the selected request's leading agent-only input when editing
that exact turn, using the existing request-sequence and transaction
checks.
- Keep the fix at the shared server input boundary so direct,
queued/grouped, Codex, Claude Code, Pi, built-in ACP, and custom ACP
paths retain the same ownership behavior.
- Bump `HOST_DAEMON_PROTOCOL_VERSION` from 169 to 170 because the
meaning of server-to-daemon turn input changed even though its wire
shape did not.

## How you verified

- Reproduced both route failures before production edits with real
server routes and in-memory SQLite: 2/2 failed.
- Added tracked regressions for the rapid second send and first-turn
edit replacement across Codex, Claude Code, and Pi; confirmed they
failed before the fix and passed after it.
- Re-ran the real-route harness after the fix: 2/2 passed.
- `pnpm exec turbo run test --filter=@bb/server --
test/public/public-thread-fork.test.ts
test/services/prompt-history.test.ts
test/threads/thread-edit-message.test.ts`: 71/71 passed.
- `pnpm exec turbo run test --filter=bb-plugin-side-chat`: 25/25 passed.
- `pnpm exec turbo run test --filter=@bb/host-daemon-contract --
test/contract.test.ts`: 37/37 passed.
- `pnpm exec turbo run typecheck --filter=@bb/server
--filter=bb-plugin-side-chat --filter=@bb/host-daemon-contract`: passed.
- `pnpm exec turbo run build --filter=@bb/server`: passed.
- `git diff --check` and `git show --check`: passed.

Fixes get-bb#2316

> AGENT GENERATED
…b#2426)

## What was wrong

A provider plugin's frontend bundle was deferred: it loaded only after a
thread of one of its providers opened, one of its pending-interaction
forms was asked for, or its own panel route opened
(`docs/provider-plugin-api.md` §5, Q30). Composer chrome the plugin
registers through `app.composer.customize` with a `new-thread` scope was
therefore absent on the New Thread page until the user opened one of
that plugin's threads in the same page load, and it vanished again on
reload or in a new window. Any UI a provider plugin wanted at boot had
to live in a second, non-provider plugin. `experimental_visibility:
"always"` did not help: it only lists the provider in the picker.

The deferral bought little. None of the four first-party provider
plugins ship an `app.tsx`, so the gate deferred zero bytes on a default
install. Third-party provider frontends on a real machine are tens to a
few hundred KB of JS, and every plugin bundle already loads in the
post-paint idle boot pass, not on first paint.

## What changed

Provider plugin frontends now load like every other plugin's: in the
deferred boot pass, whether or not one of their providers is selected.

- `apps/app/src/lib/plugin-frontend-provider-gate.ts`: deleted, with its
test.
- `apps/app/src/lib/plugin-frontend.ts`: `providerIds` dropped from
loader candidates; `selectLoadablePluginFrontendCandidates` and the
`wantedProviderPluginIds` reconcile dep removed.
- `apps/app/src/lib/plugin-frontend-lazy.ts`:
`requestProviderPluginFrontend` removed.
- Call sites removed: `ThreadDetailView`, `PluginThreadChat`,
`PluginPendingInteractionComposer`, and the route-plugin pre-want in
`usePluginFrontendBoot`.
- Docs and comments: `docs/provider-plugin-api.md` §5,
`docs/api_to_audit.md`, the `bb-plugin-authoring` skill, and the
`InstalledPlugin.providerIds` contract comment now state the eager rule.

`InstalledPlugin.providerIds` stays as an inventory field: it is in
`@bb/sdk`, the bundled plugin-sdk types, mobile, and the CLI, and it is
useful apart from the loader. No manifest change, no plugin SDK surface
change, and no wire change (frontend only, so no
`HOST_DAEMON_PROTOCOL_VERSION` bump).

## How you verified

- Tests updated for the removed gate: `usePluginFrontendBoot`,
`PluginPendingInteractionComposer`, `PluginThreadChat.provider-context`,
`plugin-frontend`, `plugin-frontend-load-order`,
`plugin-frontend-reload`. The pending-interaction test now covers only
what remains: the form resolves through the slot store once its renderer
registers.
- `pnpm exec turbo run test --filter=@bb/app` for those files plus
`plugin-frontend-lazy`, `useThreadCreationOptions`, `PluginThreadChat`,
`ThreadDetailPromptArea`: 118 tests pass.
- `pnpm exec turbo run test --filter=@get-bb/plugin-sdk`
(`provider-plugin-doc`) and `--filter=@bb/server`
(`plugin-authoring-docs`): pass.
- `pnpm exec turbo run typecheck lint --filter=@bb/app`: pass. Prettier
clean.

> AGENT GENERATED

Co-authored-by: Claude <noreply@anthropic.com>
## Human comments

## What was wrong

`AUTOMATION_PROMPT_MAX_LENGTH = 8_000` came with the original plugin
rewrite (get-bb#516) with no stated reason. It sat on the agent execution
schema, which also decodes every stored row on list, show, update, and
the scheduler sweep. The `bb automation update` CLI path commits the row
before that schema runs, so a prompt over the cap was written and then
made the row unreadable. `list` failed for the whole project, and the
`update` that would repair it read the row first and failed the same
way. Issue: get-bb#2166. Report:
https://get-bb.github.io/reports/issues/2166.html

## What changed

- `plugins/automations/src/limits.ts`: removed the constant.
- `plugins/automations/src/rpc-types.ts`: `prompt` is now
`z.string().min(1)` in the execution schema and the partial-update
schema. Removed the import and re-export.
- `plugins/automations/detail-view.tsx`: removed the `maxLength` on the
prompt textarea.
- `plugins/automations/src/frontend-imports.test.ts`: the frontend no
longer reaches `src/limits.ts`, so the guard's coverage list no longer
expects it. The zod guard is unchanged.

Deviation from the issue's proposed fixes: instead of adding
request-side validation at the CLI boundary, this removes the cap end to
end. Thread messages have no prompt cap, and a cap on the stored-row
schema makes a row unrepairable. A row that an older build already
corrupted becomes readable as soon as this version loads.

No wire change. No CLI, guide, or doc surface described the cap.

## How you verified

- New test in `plugins/automations/src/server-harness.test.ts`: a CLI
update with a 10,500-character prompt succeeds, and `list`, `show`, and
a later RPC update still read the row. It fails on the previous source
with the `too_big` rejection and passes after the change.
- `pnpm exec turbo run test typecheck build lint
--filter=bb-plugin-automations --force`: 67/67 tests pass, all tasks
green.
- `rg AUTOMATION_PROMPT_MAX_LENGTH` finds no reference in `plugins/`,
`apps/`, `packages/`, or `docs/`.

Fixes get-bb#2166

> AGENT GENERATED

Co-authored-by: Claude <noreply@anthropic.com>
## Human comments

## What was wrong

The Automations picker integration test rendered the full detail page
and then called `getByRole` three times for controls supplied by its own
lightweight SDK mocks. Each call recomputed accessible roles, names, and
visibility across the entire jsdom tree. Under the packages job's
systemic CPU oversubscription—73 package test tasks on a 4-vCPU runner
with no shard-level Turbo concurrency cap, plus Vitest file
parallelism—those avoidable accessibility transforms consumed 4.033
seconds of a measured 5.081-second body and crossed Vitest's existing
5-second hang ceiling. Scheduler contention amplified the cost; it was
not the root cause. Production behavior was correct.

## What changed

The test now locates the mocked provider control and real save control
by their exact text, and the permission control by its exact accessible
label. It still renders `AutomationDetailView`, clicks the same
controls, verifies the same environment routing and provider
reconciliation, and asserts the same persisted
provider/model/reasoning/service-tier/permission tuple. No timeout,
polling, retry budget, assertion, production behavior, wire contract,
CLI surface, or documentation changed.

## How you verified

- Pre-fix controlled reproduction: with 640 CPU workers on a 16-core
host and the test process at niceness 20, the exact test failed with the
CI signature at line 96 after 6.417 seconds. At 512 workers, temporary
operation timings measured 1.034 seconds rendering and 4.033 seconds
across the three role queries; state-changing clicks took 14
milliseconds, and the body reached its assertion at 5.081 seconds before
Vitest reported the 5-second timeout.
- Post-fix controlled reproduction: the cleaned test passed the
identical 640-worker stress command in 2.62 seconds. No clock changed.
- `pnpm exec turbo run test --filter=bb-plugin-automations --force --
--run src/automation-provider-model-picker.test.tsx` (1 passed; focused
test 53ms)
- `pnpm exec turbo run test --filter=bb-plugin-automations --force` (5
files, 66 tests passed)
- `pnpm exec turbo run test --filter=@bb/app --force -- --run
src/components/tools/detail-page-recipes.test.tsx -t 'Automation detail
recipe'` (11 passed, 21 intentionally filtered)
- `pnpm exec turbo run typecheck --filter=bb-plugin-automations
--filter=@bb/app --force` (5 Turbo tasks passed)
- `pnpm exec turbo run build --filter=bb-app --force` (11 Turbo tasks
passed, including App, plugin SDK, server, host daemon, CLI, and
packaged app)
- `pnpm exec oxfmt
plugins/automations/src/automation-provider-model-picker.test.tsx
--check`
- `pnpm exec oxlint
plugins/automations/src/automation-provider-model-picker.test.tsx`
- `git diff --check`

> AGENT GENERATED: by GPT-5.6-Sol
## Human comments

## What was wrong

Cursor's parameterized ACP picker accepts bare model IDs, but the shared
session builder forwarded previously persisted Cursor CLI-family IDs
unchanged. Existing threads and project defaults could therefore send
values such as `cursor-grok-4.6-medium` or `auto`, which Cursor rejects
before the first prompt.

## What changed

Normalize persisted model IDs only for Cursor's parameterized picker at
the shared ACP session-construction boundary. The implementation reuses
the existing CLI variant parser, maps `auto` to `default`, removes
historical variant syntax and the `cursor-` prefix, and leaves current
bare IDs unchanged. Start, resume, fork, selected-only models,
project-default starts, reasoning, and service tier share the covered
path. `HOST_DAEMON_PROTOCOL_VERSION` remains 170 because no
server-to-daemon wire contract changed; no CLI, SDK, documentation,
migration, or generated-file update is required.

## How you verified

The new start, resume, and fork cases failed before the fix with `model
not found` for `cursor-grok-4.6-medium`, `cursor-grok-4.5-medium`, and
`auto` (3 failed, 284 passed), then passed after the fix.

- `pnpm exec turbo run test --filter=@bb/provider-bridge-acp --force` —
17 files, 288 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/provider-bridge-acp
--filter=@get-bb/plugin-sdk --filter=bb-plugin-provider-acp --force` — 6
tasks passed.
- `pnpm exec turbo run build --filter=@get-bb/plugin-sdk --force` — 2
tasks passed; 17 runtime entries built.
- Focused `oxfmt --check`, `git diff --check`, and the original bridge
reproduction passed.

Fixes get-bb#1688

> AGENT GENERATED
…ge (get-bb#2434)

## Human comments

## What was wrong

Background commands that Claude Code workflow agents ran showed up in
the parent thread's "Running background command" card and sidebar count,
as if the parent had run them. The CLI emits `task_started` for every
task in the session, including tasks a workflow agent starts, but it
forwards `tool_use` blocks only from the main loop and from `Agent`
sub-agents. `task_started` carries only `tool_use_id` (the child's own
call), with no parent attribution. The bridge keyed the task on that id,
the assembler minted a parent id that no item ever claimed, and the
timeline projection treated the orphan as a root-level command. The same
`task_started` also emitted `turn.open`, so a workflow child's command
opened a provider-only turn in the parent that stayed open until the
CLI's next `result`.

Evidence from one workflow-heavy thread: 2,971 of 3,052 `local_bash`
tasks pointed at a parent call that never appeared in the log; 0 tool
calls nested under its 73 `Workflow` calls (vs. 1,945 under `Agent`
delegations); 100 of its 124 turns were opened by a stray `task_started`
and lasted 500–1,900 s each. Orphaned `local_agent` tasks from workflow
children also held the parent turn open through
`hasCompletionBlockingClaudeTasks`.

## What changed

- `plugins/provider-claude-code/src/task-translation.ts`:
`translateClaudeTaskMessage` takes `hasForwardedToolUse`. A
`task_started` whose `tool_use_id` the bridge never saw open, and that
is not a restart of a tracked task, is not materialized: no row, no
`parentRef`, no `turn.open`. Its later progress/notification events fall
through the existing unknown-task branches.
- `plugins/provider-claude-code/src/delta-translation.ts`: wires the
check to `state.startedTools.has(id)`.
- `plugins/provider-claude-code/src/delta-test-harness.ts`:
`spawningToolUseMessage` / `spawningToolUseFor` helpers; existing
hand-built `task_started` tests now prime the spawning `tool_use` as the
real stream does.

No `HOST_DAEMON_PROTOCOL_VERSION` bump: the wire format is unchanged;
the daemon only emits fewer events. The thread-view projection fallback
(`isDirectBackgroundTaskForCurrentAgent` treating a missing parent as
direct) is untouched; the orphans are removed at the source.

The ordering invariant (spawning `tool_use` streams before
`task_started`, and before the call's `tool_result`) holds in every
committed fixture and recording, in a live CLI 2.1.245 run (backgrounded
Bash: `tool_use` → `task_started` → `tool_result`; foreground Bash emits
no `task_started`), and in a live resume run (`SendMessage` to a
finished agent emits a new `task_started` whose `tool_use_id` is the new
`SendMessage` call). Mining 32 recent Claude Code thread logs (≈8,300
background tasks): all 7,051 orphans sit in threads that called
`Workflow`, none in the 22 workflow-free threads; among 1,266 legitimate
tasks there were 0 cross-turn parents, 0 results-before-task, and 0
task-first orderings.

## How you verified

- New test `ignores tasks spawned by an unforwarded child (workflow
agent)` in `task-translation.test.ts`: fails before (a row and a
`turn/started` are emitted), passes after. It also checks that a child
`local_agent` does not block the turn and that the parent's own next
background command still materializes.
- `pnpm exec turbo run test --filter=bb-plugin-provider-claude-code`:
334 pass. The one failure is `recorded/fork` in
`bridge.recorded-conformance.test.ts`, which fails identically on the
unmodified branch on this machine and whose recording contains no task
messages.
- `pnpm exec turbo run typecheck
--filter=bb-plugin-provider-claude-code`: pass. Prettier clean.
- EAP codename scan: clean for the working tree and the push range.

Related: get-bb#2224 (the `background_tasks_changed` debug rows; not changed
here).

> AGENT GENERATED

Co-authored-by: Claude <noreply@anthropic.com>
## Human comments

## What was wrong

The vendored Codex app-server types under
`plugins/provider-codex/src/generated/codex-app-server/schema/` were
last generated on 2026-08-17 and had drifted from the Codex CLI that bb
now spawns (0.149.1). The stale types hid two real gaps: Codex emits ten
server notifications that `visibility.ts` did not know about, and
`DynamicToolSpec` became a tagged union upstream (0.143.0), so bb still
sent the legacy untagged shape and only worked because Codex keeps a
legacy-format normalizer.

## What changed

- **Generated schema**: regenerated with `codex app-server generate-ts`
(stable surface, Codex 0.149.1) and re-pruned to the transitive import
closure of the hand-written importers: 235 of the 663 emitted files (33
modified, 32 new, 1 dead file `ThreadCompactStartParams.ts` removed).
The committed tree matched the stable surface byte-for-byte, and no
hand-written code uses experimental fields.
- **`visibility.ts`**: adds the ten new server notifications to both
method maps as `"unknown"` (`thread/deleted`, `thread/reverted`,
`thread/queue/changed`, `thread/project/updated`,
`thread/environment/{connected,disconnected}`, `project/changed`,
`model/safetyBuffering/updated`,
`autoApprovalReview/strictReviewRequired`,
`externalAgentConfig/import/progress`). Drops the manual
`"rawResponse/completed"` union extension now that the schema includes
it.
- **`session-params.ts`**: `toCodexDynamicTools` emits `type:
"function"`. Verified against upstream source across bb's supported
range: Codex 0.136.0 (`CODEX_MINIMUM_SUPPORTED_VERSION`) deserializes
through a struct without `deny_unknown_fields`, so the extra field is
ignored; Codex 0.149.1's `normalize_dynamic_tool_specs` accepts both the
legacy and canonical shapes.
- **Test fixtures**: new required fields (`delivery` on `agentMessage`,
`pluginId`/`scriptPath` on `commandExecution`,
`appContext`/`readOnlyHint` on `mcpToolCall`, `results` on `webSearch`,
`section`/`sectionEnteredAt`/`projectId`/`recencyAt` on `Thread`,
`cacheWriteInputTokens`, `spendControlReached`).
- **Generated README**: names the current importers and package filter
(it referenced `adapter.ts` and `@bb/agent-runtime`, which no longer
exist) and records that the committed tree is the stable surface.

No `HOST_DAEMON_PROTOCOL_VERSION` bump: these types only cross the
bridge↔Codex wire, not server↔daemon. No CLI or config surface changed.

Not wired up in this PR (flows through existing default paths): the new
`subAgentActivity` and `sleep` item types and `delivery: "async"` agent
messages. `AskForApproval` dropped `"on-failure"` upstream; nothing in
bb used it.

## How you verified

- Before the hand-written fixes, `pnpm exec turbo run typecheck
--filter=bb-plugin-provider-codex` failed with 36 errors against the new
schema (missing notification methods, `DynamicToolSpec` shape, missing
required fixture fields). After: green.
- `pnpm exec turbo run typecheck test --filter=bb-plugin-provider-codex
--force`: 24 test files, 238 tests pass. The `toCodexDynamicTools` test
expectation now asserts the `type: "function"` tag.
- EAP codename scan of the working tree (tracked and untracked) and
`HEAD`: 0 hits.

Fixes #

> AGENT GENERATED

Co-authored-by: Claude <noreply@anthropic.com>
…et-bb#2227)

## What was wrong

`bb plugin build` resolves sonner, vaul, `@pierre/diffs`, the ten portal
radix families and the host-resident
`clsx`/`tailwind-merge`/`class-variance-authority` through an esbuild
shim plus a generated export manifest, so the bundle never reads them
from `node_modules`. A plugin's `tsc` has no such shim: it resolves
those imports through ordinary node resolution, and nothing supplied
declarations for them. The scaffold only declared the four shimmed
packages its starter components happened to import
(`@radix-ui/react-dialog`, `clsx`, `tailwind-merge`,
`class-variance-authority`), so the documented `import { toast } from
"sonner"` — and 12 other shimmed specifiers — failed with `TS2307:
Cannot find module 'sonner'` in a fresh `bb plugin new --app`, even
though `bb plugin build` succeeded. The shim list was also hand-copied
in three places (the builder, the export-manifest generator, and the
scaffold generator), and the scaffold's copy had already drifted
(`@pierre/diffs` missing). Issue get-bb#2072; investigation report:
https://get-bb.github.io/reports/issues/2072.html.

## What changed

- `packages/plugin-build/src/runtime-shims.mjs` (+ `.d.mts`): the single
shim table. Plain ESM so the two generator scripts that run under bare
`node` before any TypeScript is compiled can read it by file path
(`@bb/templates` cannot depend on `@bb/plugin-build` without a workspace
cycle). Exports `RUNTIME_SLOT_BY_SPECIFIER`,
`RUNTIME_SHIM_NPM_SPECIFIERS` (what the export manifest introspects) and
`SHIMMED_TYPE_PACKAGES` (the npm packages a plugin must declare for
types: every shimmed package except React, whose types are
`@types/react*`). `build-plugin-app.ts`,
`generate-runtime-export-manifest.mjs` and
`generate-plugin-scaffold.mjs` all read it; the hand-copied
`RUNTIME_MODULE_IDS` and `SHIMMED_SPECIFIERS` lists are gone.
`turbo.json` adds the file to both generate tasks' `inputs`.
- `packages/templates/scripts/generate-plugin-scaffold.mjs`: the
generated module now emits `PLUGIN_SHIMMED_TYPE_DEPENDENCIES` (renamed
from `PLUGIN_STARTER_TYPE_DEPENDENCIES`) covering every shimmed package,
versions mirrored from `apps/app/package.json` via the existing
`versionedDeps()`. `scaffoldPlugin` writes all of them into an app
scaffold's `devDependencies`.
- `bb plugin types` (`setPluginSdkPin` in
`packages/templates/src/plugin-scaffold.ts`): alongside the SDK pin it
brings the shimmed packages' type-only devDependencies to the host's
versions — repinning a drifted range, moving a copy out of
`dependencies`, and (for `bb.app` plugins) adding any that are missing.
`--check` reports each one and exits 1. `bb plugin migrate` is unchanged
(its plan stays the SDK layout switch). The CLI prints one line per
repinned package.
- Docs: the `bb-plugin-authoring` skill (manifest rules, `bb plugin
types`, the "import freely" list), the `bb-cli` skill, the in-CLI plugin
guide (`packages/templates/src/templates/bb-guide-plugins.md`: command
reference and the shim paragraph) and the scaffold README now say
shimmed packages need a `devDependencies` entry for types at the host's
version and never belong in `dependencies`.

No wire changes; `HOST_DAEMON_PROTOCOL_VERSION` untouched. No new plugin
API members.

## How you verified

- New `packages/templates/test/plugin-scaffold-shim-types.test.ts`:
scaffolds an app plugin, links exactly the packages its `package.json`
declares into `node_modules` (no network), adds `import { toast } from
"sonner"` to `app.tsx` plus a file importing every shimmed specifier
(including `@pierre/diffs/react`), and runs the scaffold's own `tsc`.
Against the previous generator it fails with `Cannot find module
'sonner'` / `'@pierre/diffs/react'` / ...; it passes with this change.
- New guard in
`apps/cli/src/__tests__/plugin-scaffold-dependencies.test.ts`: the app
scaffold's `devDependencies` ⊇ `SHIMMED_TYPE_PACKAGES`, and none of them
is in `dependencies` — derived from the build's own table, so adding a
slot without declaring its types fails the test.
- New `setPluginSdkPin` cases in
`packages/templates/test/plugin-migrate-layout.test.ts`: an app plugin
with a drifted `sonner` range, `vaul` in `dependencies` and the rest
missing ends up with every shimmed package in `devDependencies` at the
host's version and is idempotent afterwards; a headless plugin only has
the shimmed packages it already declares repinned.
- `pnpm exec turbo run test --filter=@bb/plugin-build
--filter=@bb/templates --filter=@bb/cli` (all green), `apps/server`
`plugin-install.test.ts` (uses the scaffold; green), and `pnpm exec
turbo run build typecheck` for the whole repo (88/88).
- Live, with this worktree's CLI build (`BB_CLI_REEXEC=1` so the
installed `bb` does not take over, isolated `BB_DATA_DIR`): `bb plugin
new toasty --app` → `package.json` lists all 16 shimmed packages in
`devDependencies`; adding `import { toast } from "sonner";
toast.success("hi")` to `app.tsx` → `npx tsc --noEmit` exit 0; `bb
plugin types --check` exit 0; after `npm pkg set
devDependencies.sonner="^0.1.0"`, `--check` prints `Set "sonner" to
^1.7.4 in devDependencies — the version this bb shims at runtime` and
exits 1; `bb plugin types` prints `sonner: ^0.1.0 → ^1.7.4 in
devDependencies.`; `bb plugin build` still succeeds.

Fixes get-bb#2072

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Human comments

## What was wrong

`@anthropic-ai/claude-agent-sdk` was pinned at `^0.3.197`; the current
release is `0.3.245`. The old pin also forced a compatibility cast in
`SdkSession.applyMutableSettings`, because that SDK's `Settings` type
omitted `effortLevel: "max"`.

Separately, the recorded-conformance `fork` cell failed on macOS before
this change. `tmpdir()` is a symlink there, and the Agent SDK names a
project directory after the real path, so the harness seeded the fork
transcript under a name the SDK never looked up. Linux CI never hit
this.

## What changed

- `plugins/provider-claude-code/package.json`: SDK `^0.3.197` →
`^0.3.245`. Lockfile changes are limited to the SDK and its platform
packages.
- `plugins/provider-claude-code/src/bridge/sdk-session.ts`: the new SDK
types `max` on `applyFlagSettings`, so the
`ClaudeMutableSettingsQueryBoundary` cast is gone.
`ClaudeMutableFlagSettings` is a type alias now, because the new
`Settings` carries a string index signature that only an object type
alias satisfies implicitly.
- `bridge.test.ts` / `bridge.calibration.test.ts`: the new `CanUseTool`
requires `requestId` and may resolve to `null`; the tests pass a request
id and narrow the result.
- `packages/provider-bridge-protocol/src/testing/parity.ts`: the replay
workspace is `realpathSync`'d so the seeded fork transcript sits where
the SDK looks.
- `@get-bb/plugin-sdk` `0.4.19` → `0.4.20`
(`packages/domain/src/plugin-sdk-version.ts` +
`packages/plugin-sdk/package.json`): the `parity.ts` change ships in
`dist/provider-bridge-testing.js`, and `0.4.19` is already taken on
main. The npm version guard passes.

No wire change between server and host daemon;
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- `pnpm exec turbo run typecheck test
--filter=bb-plugin-provider-claude-code --force`: typecheck clean,
335/335 tests pass (rebased on current main).
- `pnpm exec turbo run test --filter=@bb/provider-bridge-protocol
--force`: 237/237 pass. `@bb/domain`: 172/172.
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs`: PASS
(`0.4.20` not on npm yet). Before the bump it failed on
`dist/provider-bridge-testing.js` drift, which confirms the bump is
required.
- Baseline check: with the old SDK on HEAD, the `fork` cell fails
identically on macOS, so the harness fix is not masking an SDK
regression. After the fix, `bridge.recorded-conformance.test.ts` passes
on macOS.
- Note: running the replay suites of several packages concurrently
produces spurious 5 s timeouts in `bridge-worker-entry.test.ts` and the
recorded cells; run alone, each suite is green every time.
- EAP codename scan (working tree, tracked files at HEAD, added lines
and commit messages of `origin/main..HEAD`): 0 hits in every group.

> AGENT GENERATED

---------

Co-authored-by: Claude <noreply@anthropic.com>
…2389, get-bb#2392 with the review fixes (get-bb#2435)

## Human comments

## What was wrong

Five of the mobile-perf2 PRs by @vburojevic (get-bb#2385, get-bb#2386, get-bb#2388, get-bb#2389,
get-bb#2392) were each sound in design but carried one or two confirmed
defects that an adversarial review found: the touch scrollbar thumb
never showed after the per-scroll trim, the cached scroll-anchor row
list went stale on windowed timelines, the deferred expander body
blanked its preview and animated toward an empty region, the
host-disconnect status fan-out ran ~5–7 synchronous queries for every
thread on the host, a status change that raced an in-flight search was
never refetched, the `max-age=300` app shell let browsers and the
Electron window boot a stale shell whose hashed assets 404 for up to
five minutes after an update, and the shared action-bar width was 16 px
wider than the assistant column. This PR integrates the five on current
`main` and fixes each finding, so the set can land together instead of
one at a time.

Credit: every original commit keeps @vburojevic as author, and every fix
commit carries a `Co-authored-by` trailer for them. A squash merge
credits them through those trailers; a merge or rebase keeps the
authorship as-is.

## What changed

Original work (21 commits, cherry-picked in PR order, unchanged):

- get-bb#2385 Calm the iOS shell-geometry handler and the timeline resize
cascade.
- get-bb#2386 Let taps paint: transition-priority navigation, deferred
expanders and sidebar realization.
- get-bb#2392 Consolidate per-row ResizeObservers into shared
read/write-phased observers.
- get-bb#2388 Realtime round 2: status-change metadata everywhere, reconnect
gating, coalesced fallback refetches.
- get-bb#2389 Boot and delivery quick wins for the relayed mobile path.

Fixes (13 commits; each names the finding it closes):

- `bottom-anchored-scroll-body.tsx`: the `data-scrollbar-scrolling`
write is back on every pointer, idempotent (one write per scroll burst);
`.thread-scrollbar`/`.transient-scrollbar` are not pointer-gated, so
skipping it on touch hid the thumb. The scroll-anchor row cache is
bypassed when the top-level list holds a
`[data-timeline-virtual-spacer]`; unwindowed timelines keep it. The
scroll-preservation suite now exercises the entries-derived resize path
and the scroll-gate test pins the keyboard-pan compensation.
- `disclosure.tsx`: the region's content branch, height sync, transition
classes, deadline and in-flight accounting key on the deferred expanded
value, so the collapsed preview stays until the body exists and the
tween starts from the real body; re-expanding inside the 200 ms close
window keeps the retained body.
- `session-owner-side-effects.ts` + `packages/db`
(`listActiveHostThreads`): daemon close, disconnect grace and host
removal build `statusChange` metadata only for active threads, fetched
in batched queries; idle threads keep the pre-PR bare push.
- `realtime-cache-registry.ts`: the `statusChange` search invalidation
keeps `cancelRefetch: false` and schedules one trailing refetch after
the in-flight search settles; the same two-line gap in the
completed-turn path is fixed in its own commit. The coarse-pointer
debounce test moved to its own file so the 65-test suite runs in the
shared vitest worker again.
- `server.ts`: the app shell is served `Cache-Control: no-cache` + weak
build-id ETag again; the If-None-Match → 304 path is unchanged.
`apps/connect/src/cache.ts`: the worker's revalidated-shell contract is
now `no-cache` + ETag; the edge copy is stored with an internal 300 s
bound, served only after the origin's 304, and the visitor always
receives the origin's `no-cache`. `no-store`/`private`/set-cookie still
bypass; the plain asset path still rejects `no-cache`. Known limit
(unchanged from get-bb#2389): the edge document copy is rarely served in the
mobile flow because browsers keep their own copy.
- `document-cache.test.ts`: each test stores its own edge copy.
`bundle-budget.json`: `maxBootBrotliBytes` 479,067 → 429,072 (10% above
the measured boot payload); `maxBootBytes` unchanged.
`vite-font-preload.test.ts`: head order pinned against a synthetic
document, since the dist-gated suite is skipped in CI.
- `MessageActionBar.tsx` / `ThreadTimelineRows.tsx` /
`ConversationMessageContent.tsx`: the shared list width subtracts the
assistant column's `px-2` inset; the class and the 16 px constant are
declared together.

No wire change between server and host daemon
(`HOST_DAEMON_PROTOCOL_VERSION` unchanged; get-bb#2388's one-line
`daemon-protocol.ts` edit adds no field). No CLI, guide, or doc surface
changes.

Not addressed, by decision: a deferred expander body can lag behind
heavy streaming updates until React's transition expiry (a timeout
fallback changes when the expensive render blocks the main thread, which
is the trade-off get-bb#2386 is about); the transition-priority navigation in
get-bb#2386 is a no-op because react-router already transitions (harmless,
left as-is).

## How you verified

Review: two independent multi-agent passes over each PR (correctness,
claims audit with the PR's tests run, repo-rule/contract audit, one
PR-specific lens), every finding checked by three refuters (code trace,
a throwaway experiment against the real code, an impact judge); only
findings that survived at least two of three were fixed.

Fixes: each fix commit came with a test proven to fail before and pass
after, and passed two independent verifiers (a diff reviewer and a
runner that re-proved fail-before by restoring the pre-fix sources and
ran the package suite) in one round.

Final branch (`3388bcb45`):

- `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/server
--filter=@bb/connect --filter=@bb/db --filter=@bb/desktop
--filter=@bb/mobile --filter=@bb/cli --filter=@bb/sdk --continue` — exit
0.
- `pnpm exec turbo run lint --filter=@bb/app --filter=@bb/server
--filter=@bb/connect --continue` — 0 errors.
- Full suites: `@bb/db` 409/409, `@bb/connect` 120/120 (the
document-cache suite runs the real worker in workerd: cold store,
304-only repeat, new build on next navigation, visitor 304 relayed,
pre-contract server proxied uncached), `@bb/server` 2,047/2,047,
`@bb/app` 3,404 passed / 4 skipped / 0 failed.
- `pnpm exec turbo run build --filter=@bb/app` + `node
apps/app/scripts/check-bundle-budget.mjs` — OK: boot 1,547.5 KB raw /
381.1 KB brotli, 3 boot chunks.
- EAP codename scan: clean for the working tree, tracked files, and the
added lines and commit messages of `origin/main..HEAD`.

Still to do by hand before merge: a physical iPhone and Android pass for
get-bb#2385/get-bb#2392 (keyboard open/close, URL-bar collapse, rotation with many
expanded rows); the CI iOS-simulator job is skipped. The `@bb/mobile`,
`@bb/cli`, `@bb/sdk`, `@bb/integration-tests` suites and the packaged
tarball smoke were green on an earlier nine-PR merge tree, not yet
re-run on this exact branch.

Supersedes get-bb#2385, get-bb#2386, get-bb#2388, get-bb#2389, get-bb#2392.

> AGENT GENERATED

---------

Co-authored-by: Vedran Burojevic <vedran.burojevic@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Sawyer Hood <kirbyhood@gmail.com>
## Human comments


<img width="690" height="647" alt="Screenshot 2026-08-25 at 4 03 49 PM"
src="https://github.com/user-attachments/assets/6cd7245c-286d-4d6a-9585-a40dd3da85c0"
/>

This basically upstreams https://github.com/andrewkchan/bb-plugin-monaco
as a built-in plugin.
- Optimized so that it gets bundled instead of lazy loaded with AMD
loader, now it should add only ~6MB (the typescript language services
are stripped)
- One functional difference from external monaco is that hovering
symbols in typescript files no longer shows diagnostics and CMD+click
does not work to gotodef in the same file. This is because we're
stripping the builtin monaco typescript language service. The plan is to
make the language diagnostics optional (first, add back in the monaco
builtins, then add an interface for custom LSPs as plugins, see
get-bb#2438)

-----

## What was wrong

BB's file panel is read-only. Opening a file from chat, the file search,
or `bb thread open` gives a preview, so any edit means leaving BB for an
editor — even for a one-line change to a file the agent just wrote. This
upstreams the Monaco plugin I built out-of-tree
([andrewkchan/bb-plugin-monaco](https://github.com/andrewkchan/bb-plugin-monaco))
so the panel can edit as well as show.

## What changed

**New builtin: `plugins/monaco-editor`.** A `fileOpener` that replaces
the preview with [Monaco](https://microsoft.github.io/monaco-editor/)
for ~86 text and code extensions:

- Editing with <kbd>⌘S</kbd> saves guarded by `expectedSha256`, so a
save that would clobber a concurrent write (usually the agent's) stops
and offers Reload or Overwrite.
- Find (<kbd>⌘F</kbd>), line numbers, syntax highlighting, and Monaco's
usual editing affordances.
- A file tree with path filtering, expand/collapse,
reveal-the-open-file, and a right-click menu to copy absolute path /
relative path / filename.
- Quick-palette rows for folding, sorting selected lines, and copying
the current file's path, via the `commandPaletteAction` slot from get-bb#2270.
- Type and theme matched to BB's own preview (`font-mono text-xs
leading-5`, `--font-mono`, light/dark).

Registered `defaultEnabled: true` under `Interface`. That is the
significant product change: **the file panel becomes an editor for
nearly every text file by default.** Per-extension opt-out lives in
Settings → File openers, and binaries fall through to BB's preview via
the `Original` prop.

**The plugin id is `monaco-editor`, not `monaco`.** The id derives from
the package name, and `monaco` is what the external plugin this
upstreams already uses. Reconcile refuses to displace a user's own
install, so sharing the id would have meant anyone holding that plugin
got the builtin silently skipped — nothing but a server log — until they
removed theirs and restarted. Renaming sidesteps it: the two coexist,
and removing the external one needs no special handling. The display
name is "Monaco editor" so they are tellable apart in Settings during
the transition.

**Monaco is built here, not bundled into `app.js` and not copied from
its prebuilt tree.** `scripts/stage-assets.mjs` runs esbuild over
`monaco-bundle/editor.js` and emits
`dist/monaco/{editor.js,editor.css,editor.worker.js}`;
`lib/monaco-loader.ts` loads those from a `files.createPreview` URL the
first time a file tab opens.

Bundling into `app.js` is not an option: `bb plugin build` emits one
file with no code splitting, so Monaco would parse at app boot for every
user including those who never open a file, and its worker could not be
emitted at all. Building it separately keeps it lazy — **`app.js` is 24
KB** — while letting esbuild prove reachability, which is what makes the
trimming safe.

**Size: the packaged builtin is 6.3 MB**, of which Monaco is 4.6 MB. The
entry is Monaco's own `editor.main` — the API, its contribution modules,
and every Monarch grammar — minus the CSS, HTML, JSON, and TypeScript
*language services*, which this plugin has no use for (its checker sees
only the open file, so its "cannot find module" errors would be wrong).
An earlier revision of this branch shipped Monaco's whole prebuilt AMD
tree at 25 MB.

**A generic staging hook**
(`apps/server/scripts/copy-builtin-plugins.ts`). Packaging copies only a
builtin's `dist/` and `skills/`, and builds plugins itself without
running per-plugin scripts — so a plugin needing runtime files on disk
has nowhere to put them. `copyBuiltinPlugin` now runs
`<pluginRoot>/scripts/stage-assets.mjs` when present. Optional, and this
plugin is currently the only user. A source checkout never runs that
path, so the plugin also builds its own bundle when it is missing or
older than its sources.

Also updated: `smoke-tarball.mjs`, and the two "declare metadata for
every builtin" tables in the server tests (both failed before I added
the entries, which is them working as intended). No other core code is
touched — `plugin-service.ts` and `plugin-registration.ts` are unchanged
from `main`.

## How you verified

```
pnpm exec turbo run typecheck test --filter=bb-plugin-monaco-editor    # 11 tests
pnpm exec turbo run test --filter=@bb/server -- builtin-plugins official-plugins   # 33 tests
pnpm exec turbo run test --filter=@bb/app -- useThreadFileTabs         # 20 tests
```

Unit tests cover the tree logic that is easy to get subtly wrong —
nesting flat paths, synthesising directories a truncated listing
omitted, case-insensitive sorting, and filtering, which must expand
every ancestor of a match or the match stays hidden behind a collapsed
row. Another asserts every claimed extension maps to a language the
built bundle actually registers; it failed on its first run, catching
that `json` was absent from the trimmed bundle and `package.json` would
have rendered as plain text.

**The build script asserts the bundle is complete**, because trimming it
is how this went wrong twice. It fails when the output lacks the find
widget, folding, word navigation, line sorting, grammars, or
contributions. An earlier entry (`editor.api` alone) shipped without
Monaco's contribution modules: the editor opened, highlighted, and
typed, so it looked correct, while <kbd>⌘F</kbd>, the option+arrow word
motions, and every folding command silently did not exist. The guard
reports that entry precisely. Note it checks for the find widget's own
class name rather than `actions.find`, which is present with or without
the widget and would have passed.

Ran the real packaging path (`tsx
apps/server/scripts/copy-builtin-plugins.ts`) and confirmed the shipped
builtin contains `dist/monaco/{editor.js,editor.css,editor.worker.js}`
beside `dist/server.js` and `dist/app.js`, totalling 6.3 MB, with Monaco
absent from `app.js`.

**Manual testing is in progress** in a dev desktop build. Two paths are
new in the port and have never run: project-backed workspace sources (no
environment, resolved through the project's `sources[]`) and the
`experimental_hostId` field on `PluginFileOpenerSource` — the plugin's
schema was `.strict()` and would have rejected those files outright
before this branch.

## Known gaps

Documented in the plugin README, with upstream issues where the fix is
not ours:

- No language server: no go-to-definition, find-references, or type
checking. Monaco's own TypeScript service would restore single-file
navigation, but it costs a 6.7 MB worker for one language, runs whether
or not anyone wants it, and still cannot resolve an import. get-bb#2438
proposes making language intelligence plugin-contributed instead — a
toggle for Monaco's in-browser services, and a contribution point so
people can bring their own servers. Occurrence highlighting is restored
here with a textual provider, which needs none of that.
- The tree cannot show hidden files or `node_modules` (get-bb#2093), and is
read-only — no rename/create/delete.
- Opening a file from the tree reuses the tab, so its title goes stale
(get-bb#2102).
- No "open in editor" button; that capability is not exposed to plugins.

Fixes #

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t-bb#2439)

## Human comments

## What was wrong

get-bb#2127 shipped the Monaco builtin with `defaultEnabled: true`. On a fresh
install it replaces the read-only file preview for ~86 text extensions
without the user opting in. Its user-facing name also described the
engine (Monaco) rather than what it does in BB.

## What changed

- `apps/server/src/services/plugins/builtin-registry.ts`:
`monaco-editor` now has `defaultEnabled: false`. A fresh database
registers it disabled. Existing registrations keep their stored
`enabled` state because reconciliation uses `existing?.enabled ??
bundled.defaultEnabled`, so this does not turn it off for users who
already have it.
- `plugins/monaco-editor/package.json`: manifest `bb.name` is now `File
Editor` (Settings > Plugins, plugin store).
- `plugins/monaco-editor/app.tsx`: the `fileOpener` `title` is now `File
Editor` (Settings > File openers).
- The plugin id `monaco-editor`, the directory, the package name, and
the opener id `monaco` do not change, so existing registration rows and
per-extension opt-outs still match.
- `packages/bb-app/scripts/smoke-tarball.mjs`: `monaco-editor` leaves
`EXPECTED_RUNNING_BUILTIN_PLUGINS`. That list is the default-enabled set
the package smoke waits on, and a disabled plugin never reaches
`running`.

No wire changes. No CLI or doc surfaces name the plugin.

## How you verified

- Added `ships the File Editor (monaco-editor) disabled on a fresh
database` to
`apps/server/test/services/plugins/builtin-plugins.test.ts`. It fails on
`main` (`defaultEnabled` is `true`) and passes here.
- `pnpm exec turbo run test --filter=@bb/server --
test/services/plugins/builtin-plugins.test.ts
test/services/plugins/official-plugins.test.ts`: 34/34 pass.
- `pnpm exec turbo run test typecheck --filter=bb-plugin-monaco-editor`:
typecheck clean, 11/11 pass.
- EAP codename scan of the working tree, `HEAD`, and
`origin/main..HEAD`: clean.

> AGENT GENERATED

---------

Co-authored-by: Claude <noreply@anthropic.com>
## Human comments

## What was wrong

The Claude native-roots cap test coupled a deterministic 256-root
contract assertion to a 260-plugin filesystem fixture. It created a
manifest and commands directory for every plugin, then made the registry
resolver probe all 260 plugin trees. Scheduler and filesystem contention
only amplified that unnecessary structural cost: the fixture and scan
consumed the test's 5-second budget even though the production contract
filtering was correct. This surfaced in the [packages
job](https://github.com/get-bb/bb/actions/runs/32910895673/job/98004697503)
for unrelated PR get-bb#2440; the same test passed with little headroom in the
[preceding main
job](https://github.com/get-bb/bb/actions/runs/32909294708/job/98000003411).

## What changed

Extracted the synchronous Claude root composition/filter step from
filesystem discovery. The cap regression now supplies 260 synthetic
plugin command roots directly to that seam while retaining assertions
for user-root-first ordering, the exact 256-root cap, the last kept
plugin, the first dropped root, the dropped count, and the single
warning. The existing small real-filesystem cases continue to cover
registry discovery and plugin layouts.

Runtime output and wire behavior are unchanged, so
`HOST_DAEMON_PROTOCOL_VERSION` was not bumped.

## How you verified

- Before the fix, a 22-way exact focused Turbo loop reproduced `Test
timed out in 5000ms` in 14/22 runs. Temporary phase timing showed
fixture creation alone taking 5.9–7.4 seconds under contention; runs
that reached resolution spent another roughly 4.5–5.0 seconds scanning
the plugin trees. The temporary instrumentation was removed.
- After the fix, the same 22-way loop passed 22/22; the exact test took
6–10 ms in every process.
- `pnpm exec turbo run test --filter=bb-plugin-provider-claude-code
--force -- --run src/native-roots.test.ts` — 5/5 passed.
- `pnpm exec turbo run test --filter=bb-plugin-provider-claude-code
--force` — 335/335 passed.
- `pnpm exec turbo run typecheck --filter=bb-plugin-provider-claude-code
--force` — passed.
- `pnpm exec turbo run build --filter=bb-app --force` — 11/11 Turbo
tasks passed.
- Targeted `oxfmt --check`, `oxlint`, `git diff --check`, and
debug-instrumentation grep passed.

> AGENT GENERATED: by GPT-5.6-Sol
## Human comments

## What was wrong

Exact current-lane parity replays used elapsed bridge-output silence as
their terminal condition. After the final runtime request was answered,
`replayRecording` closed bridge stdin once `lastOutputAt` had been quiet
for 750ms, even though the replay provider child and Claude SDK could
still be processing provider-internal control flow. Under scheduler
contention, that EOF landed after WebFetch opened but before its
provider tail reached the bridge, so shutdown discarded the WebFetch
completion, assistant message, usage, and `turn/completed`; the
projection consequently showed an interrupted WebFetch row. This is the
failure observed in [main CI run
32906829649](https://github.com/get-bb/bb/actions/runs/32906829649).

## What changed

Exact `planFromCurrentLane` replays now assemble the complete
current-lane plan and wait for its full grammar-accepted event count
before closing bridge stdin. That lane was emitted by the bridge under
test, so it is a deterministic completion boundary; the existing timeout
remains hang detection only. Non-exact cross-version comparisons retain
the quiet fallback because a deliberately divergent bridge can validly
emit fewer events.

A real-child regression bridge acknowledges its final request, delays
`item/completed` and `turn/completed` past a deliberately shorter quiet
period, and exits without them if stdin closes early. It fails on the
old policy and passes when replay waits for the planned lifecycle tail.

This changes test-harness process sequencing only. No server↔host-daemon
or provider bridge wire fields changed, so
`HOST_DAEMON_PROTOCOL_VERSION` remains unchanged. The published
provider-bridge testing bundle changes, so `@get-bb/plugin-sdk` is
coherently bumped from 0.4.20 to 0.4.21; no new public API member, CLI,
or user-facing configuration is introduced.

## How you verified

- Before the fix, the deterministic regression failed in 657ms with only
2 of 4 expected events.
- A temporary exact Claude replay pause immediately after recorded
WebFetch-open seq 1500 reproduced the CI signature in 2.76s: the same
missing WebFetch completion/message/usage/`turn/completed` and
interrupted projected row. With the structural fix and the same 1s
pause, the exact case passed without changing any timeout.
- Exact-cell stress: 32/32 `claude-code/web-search` replays passed in
two batches of 16 concurrent real bridges, with zero stalls and 14/14
events each.
- `pnpm exec turbo run test --filter=@bb/provider-bridge-protocol
--filter=@bb/provider-parity --force` — 238/238 protocol tests and 56/56
parity tests passed; the exact Claude case completed in 3.701s.
- `pnpm exec turbo run test --filter=bb-plugin-provider-claude-code
--force` — 335/335 tests passed; the unrelated CI-timed-out native-roots
case completed in 470ms locally.
- `pnpm exec turbo run build typecheck --filter=@get-bb/plugin-sdk
--filter=@bb/domain --filter=@bb/provider-bridge-protocol
--filter=@bb/provider-parity --force` — 8/8 tasks passed.
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` —
passed; 0.4.21 is unpublished and will be shipped by the publish job.
- The full packages Turbo test command completed 71/73 tasks on macOS.
Its only failures were two unchanged `@bb/plugin-build` assertions that
compare `/tmp/...` to macOS's canonical `/private/tmp/...`; both passed
in the PR's Ubuntu packages job.
- `git diff --check` — passed.

> AGENT GENERATED: by GPT-5.6-Sol
## Human comments

## What was wrong

bb-app and the desktop app still reported 0.39.0. Changes since that
release were not available through a stable package or desktop build.

## What changed

- Bumped `bb-app` and `@bb/desktop` to 0.40.0.
- Added the 0.40.0 changelog and web release metadata for August 26,
2026.
- Documented the File Editor, built-in plugin updates, agent provider
API, performance work, iOS TestFlight, and notable fixes.
- This release commit does not change the host daemon protocol.

## How you verified

- `node .github/workflows/check-version-lockstep.mjs`
- `pnpm exec turbo run typecheck test --filter=@bb/config
--filter=@bb/server --filter=bb-app`
- `pnpm exec turbo run smoke:tarball --filter=bb-app --force`
- `pnpm exec turbo run typecheck test --filter=@bb/web`
- `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs`
- `pnpm exec oxfmt --check CHANGELOG.md
apps/web/src/landing/changelog.ts`
- `git diff --check`
- Verified the bundled Docs, GitHub, Memory, and Tasks plugin artifacts.

> AGENT GENERATED
…esize (get-bb#2453)

## Human comments

Now themes apply to the monaco editor (we need a new plugin SDK method
to supply the VSCode theme document), and the file tree is styled better
+ is resizable.



https://github.com/user-attachments/assets/8cb44a4a-57bf-44ad-a976-d9f888dfaf81



## What was wrong

Two papercuts in the builtin Monaco file editor, reported directly
rather than in an issue.

The editor followed light/dark mode only. bb's appearance palette
reaches the built-in file preview through the resolved code theme, but
Monaco stayed on stock `vs` / `vs-dark`, so switching to Nord left the
editor's syntax colors and surface on bb's default palette. The root
cause is that Monaco cannot be themed from CSS variables — it needs the
theme document itself — and no plugin API served one.
`useResolvedCodeTheme` publishes only the theme *names*, and the
documents behind them live in `@pierre/diffs`, which plugins do not
reach.

The file tree was a fixed `max-h-64` panel. The secondary pane holding
the editor resizes by dragging its vertical divider; the tree inside it
had no equivalent, so a deep tree scrolled in a 256px window regardless
of how much room the pane had.

## What changed

Three commits: the tree resize, the SDK addition, then the plugin
consuming it.

**Plugin SDK — `experimental_useCodeTheme()`**
(`packages/plugin-sdk/src/app-contract.ts`, `app.ts`,
`testing/app.tsx`). Returns `{ mode, name, theme }`: the active
light/dark mode, the registered name of the code theme bb renders that
mode with, and the resolved VS Code theme document behind it (`type`,
`fg`, `bg`, `colors`, `tokenColors`) — the same document bb's own
highlighter paints from. `theme` is null only before the first resolve,
and holds the previous document while a palette switch resolves, so a
consumer never paints an unthemed frame. Documented in
`docs/api_to_audit.md` with what to audit before the prefix drops.

`PLUGIN_SDK_VERSION` → 0.4.21, per the plugin-API surface guard, and the
plugin's `engines.bbPluginSdk` floor points at it. No wire change:
`HOST_DAEMON_PROTOCOL_VERSION` is untouched, since nothing between the
server and the host daemon moved.

**App** (`apps/app/src/lib/plugin-code-theme.ts`, new). Implements the
hook: resolves the active palette's theme through a dynamically imported
`@pierre/diffs` (the module is megabytes of Shiki and this hook can
mount on any plugin surface), caching per theme name.
`registerResolvedCodeThemeFiles` moved out of
`pierre-worker-pool-theme.ts` into `code-theme-registration.ts` so the
hook and the worker-pool sync share one registration set — Pierre treats
a second registration of the same name as an error it logs.

**Monaco plugin** (`plugins/monaco-editor/lib/monaco-theme.ts`, new).
Translates the theme document into a Monaco theme: TextMate scopes →
Monaco token rules (Monaco matches by dotted prefix and its Monarch
tokens are spelled like scope heads — the mapping Shiki uses), 3/4-digit
hex expanded, values Monaco's rule parser would *throw* on dropped, font
styles narrowed to `italic` / `bold` / `underline`, and bb's
`bb:nord:light:1f4c9a2b`-style names mapped into Monaco's `[a-z0-9-]`
name rule. `app.tsx` themes the editor at construction and follows later
palette and mode changes; the body-level overflow-widget host follows
the applied document's base rather than the app mode, so the frame
between a mode switch and the new document resolving stays coherent.

**Resizable file tree** (`components/FileTreePanel.tsx`,
`lib/file-tree-height.ts`). The bottom edge is a `role="separator"`
divider in the same idiom as bb's own pane dividers — a hairline plus a
12px transparent hit target, pointer-captured — with arrow-key resizing.
Clamping keeps ≥96px of tree and ≥120px of editor; a pane too short for
both gives the tree its minimum rather than stranding it out of reach.
The height persists to `localStorage` (the panel unmounts whenever it is
hidden), and a `ResizeObserver` re-fits when the pane changes size,
restoring the requested height when it grows back.

**Tree surface.** The panel now sits on the editor's own
`editor.background` rather than `bg-surface-recessed`, falling back to
the token before a theme resolves. Hover and active-row tints are
translucent ink mixes, so they composite onto it correctly. Its filter
bar takes the file bar's `bg-surface-raised` and metrics so the two
stacked bars read as one strip; the filter field became a translucent
inset rather than an opaque app surface on top of the editor's.

## How you verified

- `pnpm exec turbo run typecheck test --filter=bb-plugin-monaco-editor`
— 25 tests pass. New: `lib/monaco-theme.test.ts` (14) covers short-hex
expansion, rules Monaco would reject, both spellings of a multi-scope
rule, font-style narrowing, the editor-surface fallback, and the
mid-switch base. The tree's clamp is left untested — it is a few lines
of arithmetic with no failure mode worth a fixture.
- `pnpm exec vitest run src/lib/plugin-code-theme.test.tsx` in
`apps/app` — 3 tests against the *real* highlighter, not a fake
resolver: the served document, the previous document surviving a palette
switch in flight, and a second consumer's first render coming from
cache.
- `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/domain
--filter=@get-bb/plugin-sdk --filter=bb-plugin-monaco-editor` — clean.
`@get-bb/plugin-sdk` and `@bb/domain` test suites pass (219 + …),
including the SDK version-lockstep test.
- Ran the converter against every bundled palette (pierre light/dark,
nord, dracula, gruvbox-light-medium, catppuccin-mocha): 191–432 rules
each, **zero** values Monaco's parser would throw on, and each
`editor.background` matching its palette.

Not verified: the drag feel and the painted colors on screen. Both want
an eyeball before merge.

> AGENT GENERATED

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What was wrong

get-bb#2282 published install counts on every Browse card, but the sort menu
still offered one option, "Plugin name". The store's only popularity
signal was per-card text the user had to scan for, and the grid opened
alphabetically — so a widely adopted plugin appeared wherever its name
landed.

## What changed

`apps/app/src/components/plugin/management/BrowsePluginsTab.tsx`:

- The sort menu gains an "Installs" option, and Browse now opens on it,
  descending: a store's first screen should be the plugins people
  actually install. Alphabetical stays one click away.
- `groupByPublisher` takes the mode. Install order sorts numerically,
  with entries the sidecar does not name sinking to the bottom in both
  directions — an unpublished count is unknown, not zero — and names
  breaking ties so equally installed plugins stay stable.
- Only the curated marketplace publishes counts, so a catalog with none
  disables the option and falls back to alphabetical *ascending*, rather
  than inheriting the count sort's descending direction and showing an
  unexplained Z→A grid. `changeSort` compares against the mode on
  screen, so the checked row always toggles direction.

No wire, CLI, or doc surface changes: this is a view affordance over
data the API already returns, and `bb plugin search` already prints an
Installs column (`apps/cli/src/commands/plugin.ts:918`).

## How you verified

Two tests in `BrowsePluginsTab.test.tsx`, both failing before this
change: install-count ordering (default mode and direction on first
render, the uncounted entry pinned last in both directions, and the
reset to ascending when switching back to names), and the disabled
option plus alphabetical fallback when no listing publishes a count.

- `pnpm exec turbo run test --filter=@bb/app -- BrowsePluginsTab` — 13/13
- `pnpm exec turbo run typecheck --filter=@bb/app` — clean

> AGENT GENERATED
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.