Sort Browse Plugins by install count, and default to it - #1
Closed
andrewkchan wants to merge 232 commits into
Closed
Sort Browse Plugins by install count, and default to it#1andrewkchan wants to merge 232 commits into
andrewkchan wants to merge 232 commits into
Conversation
## 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>
## 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:descending: a store's first screen should be the plugins people
actually install. Alphabetical stays one click away.
groupByPublishertakes 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.
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.
changeSortcompares against the mode onscreen, 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 searchalready prints anInstalls column (
apps/cli/src/commands/plugin.ts:918).How you verified
Two tests in
BrowsePluginsTab.test.tsx, both failing before thischange: 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/13pnpm exec turbo run typecheck --filter=@bb/app— clean