Integrate upstream 0.41.0 and the dispatch-queue rework; re-tail the fork migration - #6
Conversation
## Human comments ## What was wrong Idle session release remained behind the `providerSessionReaping` experiment after the provider restorability contract became available. Users had to enable the experiment before BB released restorable non-Codex sessions. ## What changed BB now releases every restorable provider session after 30 idle minutes. The change removes the experiment key, Settings toggle, server policy route, and host-daemon policy request. It also removes the legacy Codex-only fallback. The host-daemon protocol version increases to 175 because the internal server contract changed. The Plugin SDK version increases to 0.4.30, and the Plugin Guide inventory includes the changed declaration hash. The configuration guide, built-in CLI skill reference, generated guide source, fixtures, and tests now describe the standard behavior. ## How you verified - `pnpm exec turbo run typecheck --filter=@bb/agent-runtime --filter=@bb/host-daemon --filter=@bb/server --filter=@bb/app --filter=@bb/db --filter=@bb/host-daemon-contract` - `pnpm exec turbo run test --filter=@bb/agent-runtime --force` passed 318 tests. - `pnpm exec turbo run test --filter=@bb/host-daemon --filter=@bb/server --filter=@bb/app --filter=@bb/db --filter=@bb/host-daemon-contract --force` passed all affected suites. - `pnpm exec turbo run test typecheck --filter=@get-bb/plugin-sdk --filter=@bb/plugin-api-map --filter=@bb/app --filter=bb-plugin-plugin-api-docs` - `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` - `bb plugin build plugins/plugin-api-docs` - `node scripts/check-provider-literal-ratchet.mjs` - `git diff --check` Fixes get-bb#1604 > AGENT GENERATED
## Human comments ## What was wrong The Tasks lifecycle correctly lets a failed thread return to working, but its shared comment formatter still described every failure as terminal with “final message posted.” A recovered, actively working thread could therefore retain contradictory terminal history. ## What changed Failures now post a factual non-terminal failure comment, while completed threads retain “completed — final message posted.” Focused lifecycle coverage drives failure and recovery together and verifies that recovery changes status without adding another comment. Existing recovery, active views, completed semantics, and task status updates remain unchanged. This is plugin-local behavior with no SDK, wire, CLI, or documentation changes. ## How you verified - `pnpm exec turbo run test typecheck build --filter=bb-plugin-tasks --force` (364 tests) - `pnpm exec oxfmt --check plugins/tasks/lifecycle/index.ts plugins/tasks/lifecycle/lifecycle.test.ts` - `git diff --check origin/main...HEAD` Follow-up to get-bb#2694. > AGENT GENERATED
## Human comments ## What was wrong The parent-thread selector rendered every eligible thread in an unsearchable dropdown. As projects accumulated threads, locating a parent by title or ID became slow and error-prone. ## What changed Extract the selector into a dedicated `ParentThreadPicker` and add title/ID search, keyboard-friendly command navigation, empty results, loading and retry states, and query reset on close. The metadata row now only composes the picker and translates its selected value. Existing lazy loading, candidate ordering, eligibility filtering, selection, and responsive drawer behavior remain intact. This is app-only behavior with no wire, CLI, or documentation changes. ## How you verified - `pnpm exec turbo run test typecheck lint --filter=@bb/app --force` (449 files; 3,541 passed, 4 skipped) - `pnpm exec oxfmt --check` on all four changed files - `git diff --check` - Manual QA against parent candidates by title, ID, empty results, selection, and clearing Fixes: no linked issue. > AGENT GENERATED
## Human comments Fix for thsi: <img width="400" src="https://github.com/user-attachments/assets/caddb865-26b2-428d-83fb-8ca0bc5f713a" /> ## What was wrong Pi's provider bridge rejected a turn whenever its extracted text was empty, even when the request contained a valid local image attachment. As a result, sending a screenshot without accompanying text returned `Missing input text`. ## What changed The Pi bridge now accepts a prompt or steer request when it contains either text or images. Image-only requests are forwarded to Pi with an empty text payload, while completely empty requests remain invalid. Added lifecycle coverage for a local image-only prompt. No wire protocol, CLI, or documentation changes were required. ## How you verified - `cd plugins/provider-pi && pnpm exec vitest run --config vitest.config.ts src/bridge/bridge.lifecycle.test.ts --reporter=dot` - 1 test file passed, 9 tests passed. - The new test failed initially because its test request ID did not match the repository's allowed format; after correcting the fixture, it passed. > AGENT GENERATED
## Human comments ## What was wrong The new-thread branch hook kept its explicit base branch only in component-local React state. Route navigation unmounted the composer and discarded that state. The verified report is https://get-bb.github.io/reports/issues/2743.html. ## What changed The new-thread selection scope now retains the branch and its project-environment scope key in memory. Component-local plugin composers still reset on unmount. The change has no wire, CLI, guide, dependency, schema, or stored-data effect. ## How you verified - The new remount regression test failed before the production change: one failed test received `undefined` instead of `origin/release`. - The focused test passed after the change: 2 tests passed. - The relevant app tests passed: 91 tests in 4 files. - `pnpm exec turbo run typecheck --filter=@bb/app` passed. - `pnpm exec turbo run lint --filter=@bb/app` passed with 0 errors. - `git diff --check origin/main..HEAD` passed. - The EAP model-name range scan was clean. - The change contains 119 text lines across 3 files. Fixes get-bb#2743 > AGENT GENERATED Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
## Human comments ## What was wrong The host terminal environment did not advertise OSC 8 support, so programs using hyperlink capability detection could flatten named hyperlinks into display text and discard the target. The shared terminal also did not install an OSC 8 activation handler. Its context menu did not snapshot xterm's logical selection before xterm handled a macOS secondary click, so xterm could replace a soft-wrapped selection with the clicked word before Copy read it. See get-bb#2469 and the [reproduction and root-cause report](https://get-bb.github.io/reports/issues/2469.html). ## What changed - Advertise hyperlink support to spawned terminal processes with `FORCE_HYPERLINK=1`. - Route shared-app OSC 8 activation through bb's URL navigation after disclosing and confirming the exact target in bb's responsive dialog; visibly detected URLs retain direct activation. - Add terminal context-menu actions for opening/copying links and copying the exact xterm selection across soft-wrapped rows. - Snapshot the logical selection during context-menu capture, before xterm's nested macOS handler can replace it. - Add regression coverage for the host environment, shared-app handlers, dialog behavior, and the real xterm context-menu event sequence. - Keep the implementation in the host daemon and shared app. There are no native mobile terminal changes because mobile now uses the shared app terminal UI. - No `HOST_DAEMON_PROTOCOL_VERSION` bump is needed because this changes only the environment of newly spawned PTYs; no server/daemon wire field or message changed. - `FORCE_HYPERLINK` is the available opt-in for programs that do not recognize bb as a terminal. Programs that treat it as an unconditional override may also emit OSC 8 when their output is redirected; callers requiring plain redirected output can set `FORCE_HYPERLINK=0` for that command. ## How you verified - `pnpm --filter @bb/app exec vitest run --config vitest.config.ts src/components/thread/terminal` — 42 tests passed across 7 files. - `pnpm --filter @bb/host-daemon exec vitest run --config vitest.config.ts src/terminals/terminal-manager.test.ts` — 29 tests passed. - `pnpm exec turbo run typecheck lint --filter=@bb/app --filter=@bb/host-daemon` — all tasks passed; lint reported 0 errors. - `pnpm run build` — all 12 production build tasks passed. - Oxfmt and `git diff --check` passed for the changed source files. - Exercised the flow with Computer Use against an isolated worktree instance: a concealed OSC 8 label disclosed its exact target in bb's confirmation dialog, Open launched that target in a new browser tab, and a multi-row soft-wrapped selection copied and pasted back as one continuous logical value. Fixes get-bb#2469 > AGENT GENERATED
… leaks (get-bb#2675) ## Human comments ## What was wrong node-pty 1.1.0 leaks three file descriptors per spawned terminal on macOS: - The cleanup loop in `pty_posix_spawn` (`for (; count > 0; count--)`) never closes `low_fds[0]`. One `/dev/ptmx` master leaks per spawn. - The parent's copy of the slave fd is never closed. - The kqueue opened by `SetupExitCallback` is never closed. The host daemon spawns a pty per terminal session, so a long-lived daemon drains the system-wide pool of 511 pty devices. On the machine that hit this, the daemon held 505 leaked masters after 22 hours. No app on the host, Ghostty included, could open a new terminal. Upstream fixed all three leaks in microsoft/node-pty#882 and microsoft/node-pty#931. ## What changed - `node-pty` 1.1.0 → 1.2.0-beta.15 in `apps/host-daemon` and `packages/bb-app`, plus the lockfile. - New darwin-only regression test: `apps/host-daemon/src/terminals/node-pty-fd-leak.test.ts` spawns five ptys and asserts the `/dev/ptmx` fd count returns to baseline. CI's macOS 15 runners execute it. Why a beta: npm `latest` is still 1.1.0, and the fixes exist only on the 1.2.0 line. VS Code ships this exact version in production (`^1.2.0-beta.15` in `package.json` and `remote/package.json`). The only typings change since 1.1.0 is an optional `pixelSize` argument on `resize`. Nothing on the wire changes, so no `HOST_DAEMON_PROTOCOL_VERSION` bump. ## How you verified - The new test fails on 1.1.0 (`expected 5 to be less than or equal to 0`) and passes on 1.2.0-beta.15. The failing test is committed before the fix. - A standalone 25-spawn repro leaks 25 masters on 1.1.0 and zero on the beta. - `pnpm exec turbo run test --filter=@bb/host-daemon`: 561 passed. One pre-existing flake in `host-branches-dispatch.test.ts`, passes standalone, unrelated. - `pnpm exec turbo run typecheck --filter=@bb/host-daemon --filter=bb-app`: passes. > AGENT GENERATED
## Human comments ## What was wrong Cursor skill discovery covered conventional skill directories, but not plugins installed under the local plugin directory or completed marketplace cache. The composer therefore omitted skills and commands supplied by installed Cursor plugins. This is a follow-up to get-bb#1292. ## What changed - Added a Cursor native-roots resolver for local and marketplace plugins. - Parsed Cursor plugin manifests and forwarded declared skills and commands. - Selected the newest completed marketplace revision and ignored incomplete cache entries. - Deduplicated local links that resolve to the same cached plugin. - No host-daemon protocol change. ## How you verified - pnpm exec turbo run test typecheck --filter=bb-plugin-provider-acp --force - 81 provider ACP tests passed across 8 files. - Provider ACP typecheck passed. > AGENT GENERATED
## Human comments ## What was wrong Project branch listing exposed the app cache policy as a public `refresh` choice. Plain `bb project branches` and `sdk.projects.branches()` therefore defaulted to cached refs and could omit a newly pushed remote branch unless callers knew to request a blocking refresh. ## What changed Make the public CLI/SDK branch operation always wait for the existing bounded best-effort remote fetch, remove `--refresh` and its request field, and add a fixed cache-first branch-options route for app initialization. The app still initializes responsively and switches to the public blocking operation when the branch picker requests fresh remotes. The generated CLI Guide was updated; the bb-cli skill already documents only the now-canonical plain command. The checked-in Plugin SDK declaration inventory was regenerated for the intentional nested `BbSdk` type change; its existing generic SDK Guide card remains accurate. No host-daemon wire shape changed, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. ## How you verified - `pnpm exec turbo run test typecheck --filter=@bb/app --filter=@bb/cli --filter=@bb/server --filter=@bb/server-contract --filter=@bb/sdk --force` (734 test files; 6,284 passed, 3 skipped; all typechecks passed) - `pnpm exec turbo run build --filter=@bb/app --filter=@bb/cli --filter=@bb/server --filter=@bb/server-contract --filter=@bb/sdk --force` (7 tasks passed) - `pnpm exec turbo run lint --filter=@bb/app --force` (0 errors) - `pnpm exec turbo run test --filter=@bb/plugin-api-map --force` (73 passed; reproduces the inventory failure before regeneration and passes after) - Plugin Guide required test/typecheck workflow (11 tasks passed; Plugin SDK 220, Plugin API Map 73, app 3,551 passed / 3 skipped) - `bb plugin build plugins/plugin-api-docs` - `pnpm exec oxfmt --check` on all changed supported files - `git diff --check origin/main...HEAD` Fixes: no linked issue. > AGENT GENERATED
## Human comments ## What was wrong Cold app loads had multiple owners for the same system-config and plugin-list resources, while pending-interaction query options accidentally overwrote the shared QueryClient freshness default with `undefined`. The plugin frontend loader also interpreted a failed shared query as an authoritative empty installation set, and overlapping initial-connection atom refreshes repeated local daemon discovery. ## What changed The app now has one shared QueryClient for React and non-React owners, with stable shared query options for system config and the installed-plugin list. Plugin frontend reconciliation reuses that query and preserves active frontends when inventory loading fails. Pending interactions inherit the app freshness default unless callers provide an override, while stale remounts still revalidate. The redundant local-daemon connection refresh was removed, cache writers use the raw installed-plugin array shape, and narrow regressions cover each ownership and failure boundary. There are no wire, daemon protocol, CLI, guide, or public Plugin SDK changes. Review requested from @slopcop. ## How you verified The regressions failed before the fixes and pass afterward. Browser verification used the copied production repro database and confirmed one cold request each for system config, plugins, thread include, interactions, daemon status, and workspace targets. Hosts and sidebar retain their intentional initial-connect race-closing revalidation. - `pnpm exec turbo run test --filter=@bb/app --force`: 443 files passed; 3,510 tests passed and 3 skipped - `pnpm exec turbo run typecheck --filter=@bb/app --force`: passed - `pnpm exec turbo run build --filter=@bb/app --force`: passed - `pnpm exec turbo run lint --filter=@bb/app --force`: 0 errors - Touched-file `oxfmt --check` and `git diff --check`: passed Fixes: none > AGENT GENERATED --------- Co-authored-by: Sawyer Hood <kirbyhood@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments ## What was wrong Narrow coarse-pointer layouts did not have surface-specific icon sizing. The File Preview copy action remained a 20px target with a 12px glyph beside existing 36px/20px header actions, while attachment remove controls coupled their visible treatment and layout footprint to their hit target. A global icon-sizing rule would overcorrect dense surfaces and require unrelated core and plugin controls to opt into or out of that policy. ## What changed - Size the File Preview copy and open-in-editor actions to the existing 36px target and 20px glyph treatment on narrow coarse pointers. - Enlarge the tab close target only for the three core file-preview tab kinds; browser, terminal, and plugin tabs retain their existing behavior. - Give image and file attachment remove controls a compact 28px coarse-pointer hit target while preserving the 16px visible affordance and file-chip layout footprint. - Keep the change app-core only. There are no plugin, Plugin SDK, shared global sizing, host protocol, CLI, guide, or documentation changes. ## How you verified - Added focused regression coverage that fails without the surface-specific classes and file-preview opt-in. - `pnpm exec turbo run test --filter=@bb/app -- --run src/components/promptbox/AttachmentPreview.test.tsx src/components/secondary-panel/FilePreview.test.tsx src/components/secondary-panel/SecondaryPanelTabStrip.test.ts src/components/ui/tab-pill.test.tsx` (34 tests passed). - `pnpm exec turbo run typecheck --filter=@bb/app`. - `pnpm exec turbo run lint --filter=@bb/app` (0 errors; 173 pre-existing warnings). - `git diff --check origin/main...HEAD`. - Used the dev browser at a narrow coarse-pointer viewport and captured honest before/after screenshots for both affected surfaces. File Preview moved from a 20px/12px copy control to 36px/20px, matching its neighboring actions; attachment removal moved from a 16px target to a 28px target while keeping a 16px visible control and unchanged chip layout. Fixes: no linked issue. > AGENT GENERATED
## Human comments
## What was wrong
Conversation outlines still materialized large tool-call and
background-task payloads that cannot affect the outline, then decoded
and projected them synchronously on the server event loop. The bounded
route LRU made repeated reads fast only until the server restarted;
reopening a stable 42,033-event thread rebuilt 9,666 rows and 18.70 MiB
to produce a 698-item, 211,114-byte response.
## What changed
- Select only structurally necessary outline data: successful structural
completions with an earlier start are omitted, completion-only and
failed events remain, and unused tool/background-task fields are removed
in SQL. The query stays inline and uses the existing lifecycle indexes;
no new event index was required.
- Persist one versioned outline per stable (`idle` or `error`) thread.
Exact reuse requires the outline-event revision plus provider id/display
name, thread status, title, and fallback title to match. Active threads
keep the existing bounded in-process LRU and do not write intermediate
revisions.
- Add a generated Drizzle migration for the cascade-owned outline table
and a reproducible route/query/event-loop benchmark.
- There is no worker, worker contract, lifecycle, build entry, or worker
test. There are no retained-output, hydration, pruning, sweep, watcher,
environment, daemon-wire, CLI, SDK, or public API contract changes.
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.
## How you verified
### Reproduction
I used one SQLite backup on the same Apple Silicon Mac for both
revisions. The copied database contains 1,006 threads, 1,115,607 events,
and 1,724,880,138 event-payload bytes. The measured stable error thread
(`thr_gcuc46ug4j`) contains 42,033 events / 64,886,072 payload bytes;
its outline contains 698 items / 211,114 response bytes.
```bash
benchmark_dir=$(mktemp -d)
sqlite3 "$SOURCE_DB" ".backup '$benchmark_dir/bb.db'"
sqlite3 "$benchmark_dir/bb.db" "SELECT COUNT(*) FROM threads; SELECT COUNT(*), SUM(length(data)) FROM events;"
outline_branch=bb/pr-faster-conversation-outlines-no-worker-thr_3cmx4izjsx
outline_ref=origin/$outline_branch
git fetch origin main "$outline_branch"
git worktree add "$benchmark_dir/base" origin/main
git worktree add "$benchmark_dir/after" "$outline_ref"
cp "$benchmark_dir/after/apps/server/scripts/benchmark-conversation-outline.ts" \
"$benchmark_dir/base/apps/server/scripts/benchmark-conversation-outline.ts"
(cd "$benchmark_dir/base" && pnpm install --frozen-lockfile && \
node --conditions=source --import tsx \
apps/server/scripts/benchmark-conversation-outline.ts \
"$benchmark_dir/bb.db" thr_gcuc46ug4j 30 100 30 > "$benchmark_dir/before.json")
(cd "$benchmark_dir/after" && pnpm install --frozen-lockfile && \
node --input-type=module --conditions=source --import tsx -e \
'import { createConnection, migrate } from "./packages/db/src/index.ts"; const db = createConnection(process.argv[1]); migrate(db); db.$client.close();' \
"$benchmark_dir/bb.db" && \
node --conditions=source --import tsx \
apps/server/scripts/benchmark-conversation-outline.ts \
"$benchmark_dir/bb.db" thr_gcuc46ug4j 30 100 30 > "$benchmark_dir/after.json")
```
`cold` means a fresh route LRU with no persisted outline but warm
SQLite/OS page caches. `warm` means the same route instance after one
priming read. `reopened` means a fresh route LRU after one stable read.
Route cold/reopened and raw query/build use 30 iterations; warm uses
100.
| Outline route | Main p50 / p95 / max | Branch p50 / p95 / max |
|---|---:|---:|
| Cold miss | 115.598 / 138.960 / 148.725 ms | 74.120 / 83.996 / 90.342
ms |
| Warm in-process hit | 0.603 / 0.849 / 2.334 ms | 0.556 / 0.702 / 1.127
ms |
| Reopened stable thread | 112.349 / 129.917 / 130.892 ms | 1.124 /
1.350 / 1.437 ms |
| Query/build work | Main | Branch |
|---|---:|---:|
| Selected rows | 9,666 | 6,691 (-30.8%) |
| Selected payload | 19,608,409 bytes (18.70 MiB) | 3,967,029 bytes
(3.78 MiB, -79.8%) |
| Raw query p50 / p95 / max | 30.111 / 38.511 / 39.057 ms | 25.998 /
28.289 / 29.888 ms |
| Raw build p50 / p95 / max | 113.850 / 140.609 / 152.361 ms | 74.527 /
90.017 / 92.361 ms |
`EXPLAIN QUERY PLAN` on main used `events_thread_type_sequence_idx`, two
`events_thread_type_item_kind_sequence_idx` arms, and the covering
background-task index, with no table scan. The branch retains those
plans and adds the completion fallback lookup through
`events_item_lifecycle_thread_item_sequence_idx`; it also has no table
scan. The query-plan test protects all of these indexes.
The zero-delay timer around a cold in-process route read measured
116.928 / 139.723 / 149.481 ms p50/p95/max on main and 75.060 / 84.571 /
91.002 ms on the branch. A reopened persisted hit measured 1.758 / 2.549
/ 2.724 ms on the branch. Because there is deliberately no worker, the
first uncached build remains synchronous and can still block the event
loop for roughly 75 ms p50 on this workload. Synchronous handlers
serialize requests, so promise-based request coalescing would add no
benefit; stable durable reuse and the existing LRU avoid the repeat work
instead.
Correctness checks rebuilt every outline in the production copy before
and after: all 1,006 thread hashes were identical (42,031 total items;
aggregate result SHA-256
`4c229e658d255d0337b9a20e2ddc646bf2ef9902a3a33bd376185b78f4dabf6d`).
Main has no separate raw/uncapped conversation-outline mode. Tests cover
exact persisted reuse, metadata invalidation, downward sequence rewinds,
active-thread non-persistence, relevant-event invalidation,
timeline-only reuse, structural completion boundaries,
completion-only/failure preservation, attachments, and the index plan.
The structural-row regression fixture returns `[1, 2, 6, 7]` on main and
`[1, 2, 6]` with this change.
- `pnpm exec turbo run test --filter=@bb/db --force` — 410 passed.
- `pnpm exec turbo run test --filter=@bb/server --force` — 2,075 passed.
- `pnpm exec turbo run typecheck --filter=@bb/db --filter=@bb/server` —
passed.
- `pnpm exec turbo run build --filter=@bb/db --filter=@bb/server` —
passed.
Fixes: N/A — performance follow-up.
> AGENT GENERATED
## Human comments ## What was wrong Sticky section headers and parent thread rows explicitly used `bg-sidebar`, while transparent leaf rows inherited the sidebar content scroll surface. When that surface was rendered over the canvas, the two paths resolved to visibly different backgrounds. ## What changed Added `bg-sidebar` to `SidebarContent` so transparent rows and sticky rows share the same theme-derived surface. Added a focused component regression test for that styling contract. This is UI-only and introduces no host daemon protocol or wire changes. ## How you verified - `pnpm exec turbo run test --filter=@bb/app -- --run src/components/ui/sidebar.test.tsx` — 22 tests passed. - `pnpm exec turbo run typecheck lint --filter=@bb/app` — typecheck passed; lint completed with 0 errors and 173 existing warnings. - Dev-browser visual and computed-style checks confirmed parent, child, and standalone rows resolve to the same sidebar surface. No linked issue. > AGENT GENERATED
## Human comments ## What was wrong `apps/desktop/test/bb-process.test.ts` waited for real child-process readiness by polling a buffered log every 10 ms behind a fixed one-second wall-clock deadline. The AppImage lifecycle case serially starts a supervisor, bridge, and grandchild, so package-shard CPU oversubscription can delay `grandchild=` beyond that deadline even when the process tree is correct. The flake was therefore caused by synchronizing on elapsed wall time instead of the observable child output. ## What changed The test helper now synchronizes on stdout/stderr `data` events from the supervised process. It first accepts already-buffered output, closes the check/subscribe race with a post-subscription buffer check, rejects if the supervised process exits before the marker, and removes its stdout, stderr, and exit listeners on every settlement path. Existing test teardown remains the containment boundary if a test body times out. No timeout, polling interval, or retry budget was increased. The lifecycle assertions are preserved: the supervisor owns the process group, remains alive while its descendant runs, the descendant shares the supervisor PGID, and stop removes it. This is test-only. There is no host-daemon wire/protocol change, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. There is no CLI, SDK, guide, or user-facing configuration change. ## How you verified - Takeover gate on Intel host `host_nwqfteeqz4`: clean initial HEAD and remote PR ref `c952224c1c31691bb7d7708e1f5cba9617d42314`; initial `origin/main` `2c9f82548471c7fb160ab343c7dd8ce74bbecbc1`; initial merge-base `a2d47f5a6e7779c1d30f3b297582dc342878faec`. - Read the complete PR discussion, review threads, and checks: no comments or reviews were present; every original required check passed. Searched open work and current-main history. PR get-bb#2583 changes the later AppImage smoke readiness boundary and does not overlap this helper; current main only removed a now-forbidden comment from this file. - Independently inspected the original Linux package-shard failure ([job 98638300425](https://github.com/get-bb/bb/actions/runs/33106688728/job/98638300425)): `bb-process.test.ts` failed only the supervisor case after 1.429 s with `Timed out waiting for log line: grandchild=`, while the package job was running 68 Turbo tasks. The same run/head rerun ([job 98639685530](https://github.com/get-bb/bb/actions/runs/33106688728/job/98639685530)) passed the file 8/8 and the target case in 575 ms. - Fresh Linux red/green on a temporary Ubuntu 26.04 x86_64 VM running on the same Intel i5 host, limited to two vCPUs. Under 12 concurrent supervisor → bridge → grandchild topologies, the unchanged one-second polling wait timed out 24/24 times although every marker arrived (p50 3649.9 ms, p95 3904.3 ms, max 3946.8 ms). Under identical load, the event-driven wait completed 24/24 (p50 3147.4 ms, p95 3252.0 ms, max 3264.6 ms). Every run also verified supervisor PGID ownership/survival, descendant PGID, stop/removal, and post-run process cleanup. - Linux helper cases passed for already-buffered output, early exit rejection, simulated timed-body teardown containment, and listener cleanup. The VM and every load process were hard-bounded and removed after the run. - Merged current `origin/main` normally, including its final advance to `fb05e93ff5ad4f58831e804b973f2de790557afa`; final branch merge-base is that current-main commit and the PR delta remains only `apps/desktop/test/bb-process.test.ts`. - `pnpm exec turbo run test --filter=@bb/desktop -- --run test/bb-process.test.ts` (7 passed, 1 Linux-only skip on macOS; the real Linux case is covered above). - `pnpm exec turbo run test --filter=@bb/desktop --force`: the target file and 37/38 desktop files passed (246 passed, 1 skipped). The only local failure was current-main's unrelated Electron GUI preload smoke, which cannot reach its page in this remote macOS session; it reproduces in isolation and does not touch this PR's file. - `pnpm exec turbo run typecheck --filter=@bb/desktop --force` (3 Turbo tasks passed). - `pnpm exec turbo run build --filter=@bb/desktop --force` (12 Turbo tasks passed), followed by a latest-main incremental build (12 tasks passed; app, bb-app, and desktop rebuilt). - `pnpm exec oxlint apps/desktop/test/bb-process.test.ts`. - `pnpm exec oxfmt apps/desktop/test/bb-process.test.ts --check`. - `git diff --check`. - Final PR CI at `49ff22f3239874dbececaaaf2ade286c30456b42`: 13 checks passed, 0 failed or cancelled, and 2 expected jobs skipped; the Linux package test shard and both package-smoke jobs passed. > AGENT GENERATED: by GPT-5.6-Sol
## Human comments ## What was wrong The Linux AppImage lifecycle smoke treated the server listener's first successful `/health` response as proof that the owned bb stack was ready. The listener opens before `pluginService.start()` settles, and the desktop returns from its initial server probe before `bb-app` has finished starting and connecting the host daemon. The smoke could therefore tear down the GUI while server startup work was still in flight, then misreport a later timed-out health request as `bb became unhealthy after the GUI mount teardown`. Fresh Intel x86_64 validation against current main (`2c9f82548471c7fb160ab343c7dd8ce74bbecbc1`) reproduced that exact signature on the first real AppImage run. It also showed that the original PR's daemon-connected condition alone was insufficient on current main: that version reproduced the exact failure in 3/5 real AppImage runs without synthetic load. A daemon can connect before plugin startup settles. The separate pre-runtime `SIGTRAP` fixed by [get-bb#2657](get-bb#2657) occurs before `owned-runtime.json` exists and is not this failure. ## What changed - Merged current main normally, retaining the atomic owned-runtime publication from [get-bb#2561](get-bb#2561), the Linux smoke sandbox/stdio fixes from [get-bb#2657](get-bb#2657), and packaged-smoke exit settlement from [get-bb#2663](get-bb#2663). - Before GUI teardown, the lifecycle smoke now requires the isolated host daemon's `/status` response to report `connected: true` for the expected server URL. - It then calls the existing `/api/v1/system/providers` endpoint, whose handler waits on `providerRegistry.whenRegistrationsSettled()`. This is the direct signal that asynchronous plugin startup has settled; plugin startup failure also settles accurately rather than hanging readiness. - If the GUI exits during this readiness phase, the smoke reuses the existing bounded stdio-flush diagnostic before reporting its exit. GUI and owned-runtime death still fail immediately. The post-teardown contract is unchanged: the owned runtime PID must remain live, its independent AppImage mount must remain active, and all three health requests must succeed. No timeout, polling interval, retry budget, protocol, product runtime, or packaged behavior changed, so no host-daemon protocol bump is needed. ## How you verified All reproduction, build, test, typecheck, smoke, and load work ran on Intel host `host_nwqfteeqz4` in a temporary local x86_64 Ubuntu 24.04 VM with working FUSE; the VM was deleted after a zero-leak process/mount/temp-root audit. - Red, current main: the first real x64 AppImage run reproduced `bb became unhealthy after the GUI mount teardown`. - Red, inherited daemon-only candidate: the exact failure reproduced in 3/5 real AppImage runs without synthetic load. - Green, final direct-settlement candidate: 5/5 consecutive real AppImage runs passed, then the official Turbo smoke passed after the exact-final AppImage rebuild. - Bounded load: 3/3 additional real AppImage runs passed while two of four guest CPUs ran at 75%; every load process was stopped after its run and every cleanup audit was empty. - `pnpm exec turbo run desktop:build:linux --filter=@bb/desktop --cache-dir=.turbo/cache --output-logs=new-only`: 13 tasks passed on the final commit. - `xvfb-run -a pnpm exec turbo run smoke:appimage-lifecycle --filter=@bb/desktop --force --cache-dir=.turbo/cache --output-logs=new-only`: passed on the final AppImage. - `pnpm exec turbo run test --filter=@bb/desktop --force`: 38 files, 248 tests passed on Linux. - `pnpm exec turbo run typecheck --filter=@bb/desktop`: 3 tasks passed. - `pnpm exec turbo run build --filter=@bb/desktop`: 12 tasks passed. - `node --check apps/desktop/scripts/smoke-linux-appimage-lifecycle.mjs`, focused `oxfmt --check`, and `git diff --check`: passed. > AGENT GENERATED: by GPT-5.6-Sol
## Human comments ## What was wrong `buildPluginApp` always requested a complete esbuild metafile and canonicalized every bundled input with `realpath()`, even when no direct dependency declared `bb.pluginTailwindContent`. The fresh CLI scaffold in [main CI run 33101856455](https://github.com/get-bb/bb/actions/runs/33101856455/job/98621342155) bundles 5,154 inputs but none of its four linked runtime dependencies opts into dependency Tailwind scanning. Under package-shard contention, that unused manifest and canonicalization pass pushed the end-to-end scaffold test to 5,424 ms and beyond Vitest's unchanged 5,000 ms default. ## What changed Resolve dependency Tailwind sources before the frontend bundle, request esbuild's metafile only when at least one direct dependency opts into scanning, and canonicalize bundled inputs only on that path. The scaffold regression test delegates to real esbuild through a guard that rejects an unnecessary metafile while retaining real scaffold generation, dependency links, esbuild and Tailwind transforms, artifact assertions, and dynamic bundle import. The existing symlinked dependency-content test continues to exercise metafile-backed opt-in scanning. The branch includes current `origin/main` at `fb05e93ff5ad4f58831e804b973f2de790557afa`. The only fix-specific current-main adaptation was removing the new explanatory TypeScript comments required by the repository's no-comments policy. There are no timeout, retry, wire, CLI behavior, guide, or documentation changes. ## How you verified - Fresh Intel-only takeover on `host_nwqfteeqz4` (`x86_64`, Intel Core i5). - Original failure log: the exact scaffold case took 5,424 ms and timed out at the unchanged 5,000 ms default while 495 sibling CLI tests passed. - Final-current-main red boundary: restoring `metafile: true` made the exact scaffold test fail in 2,747 ms with `unexpected esbuild metafile: 5154 inputs`. - Final-current-main green boundary: the conditional implementation passed the same exact test in 2,316 ms without requesting a metafile. - Opt-in preservation: the symlinked workspace-dependency Tailwind scan passed in 212 ms, retaining the bundled `.tracking-widest` utility while excluding unbundled and negated content. - Earlier bounded contention evidence: 16/16 timed out before at 5.418–6.159 seconds; 16/16 passed after at 2.007–2.839 seconds. The takeover used the deterministic current-main red/green instead of repeating that stress fan-out. - `pnpm exec vitest run src/__tests__/plugin-build.test.ts --reporter=verbose` from `apps/cli`: 10/10 passed. - `pnpm exec vitest run src/build-plugin-app.test.ts --reporter=verbose` from `packages/plugin-build`: 11/11 passed. - Final-main `TMPDIR=/private/tmp pnpm exec turbo run test --filter=@bb/cli --filter=@bb/plugin-build --force --env-mode=loose --concurrency=2`: 8/8 tasks; CLI 503/503; plugin-build 134 passed, 1 skipped. - Final-main `pnpm exec turbo run typecheck --filter=@bb/cli --filter=@bb/plugin-build --concurrency=2 --force`: 5/5 tasks, zero cache hits. - Final-main `pnpm exec turbo run build --filter=@bb/cli --filter=@bb/plugin-build --concurrency=2 --force`: 4/4 tasks, zero cache hits. - `pnpm exec oxfmt apps/cli/src/__tests__/plugin-build.test.ts packages/plugin-build/src/build-plugin-app.ts --check`. - `pnpm exec oxlint apps/cli/src/__tests__/plugin-build.test.ts packages/plugin-build/src/build-plugin-app.ts`. - `git diff --check`. > AGENT GENERATED: by GPT-5.6-Sol
## Human comments ## What was wrong PR get-bb#2773 correctly retires a provider bridge after its final thread ends, but the shared process manager did not record that retirement while it was in progress. A same-key `ensureProvider` could therefore reuse the bridge being terminated; the new thread would reject immediately or briefly start and then lose its provider when the old bridge exited. Review exposed two full-shutdown ordering cases: a retirement-blocked start could spawn after the shutdown sweep, and waiting for that replacement to finish initialization could exceed the daemon's 15-second exit grace because provider RPC initialization waits up to 30 seconds. ## What changed The provider process manager now memoizes same-key retirement promises. New starts wait for retirement before selecting or spawning a bridge, and concurrent shutdown callers share the active retirement. Full runtime shutdown becomes terminal before any lifecycle wait: retirement-blocked or later ensures fulfill without spawning, while already-initializing starts treat shutdown as superseding their internal RPC wait. Shutdown then terminates every process already visible to its sweep. Final-thread retirement, bridge reuse while another thread remains, and later cold resume remain unchanged. The change is internal to `@bb/agent-runtime`; no daemon wire contract, protocol version, public API, CLI, or configuration changed. ## How you verified - Added a regression that overlaps same-key provider shutdown and ensure. It failed before the production change with `Provider "fake" is not running` and passes afterward with a distinct surviving replacement process. - Added a regression that overlaps retirement, a same-key ensure with a three-second delayed initialization response, and full shutdown. It was red because shutdown took the full initialization delay; it is green within the one-second bound, all calls fulfill, and no provider remains. A post-shutdown ensure also fulfills without recreating a provider. - `pnpm exec turbo run test typecheck --filter=@bb/agent-runtime --force` — 22 files / 318 tests passed; typecheck passed. - GitHub CI run `33459198901` — all non-skipped jobs passed, including Linux AppImage lifecycle smoke. - `git diff --check origin/main...HEAD` — passed. - Rebased and reran validation on `origin/main` at `fb05e93ff5ad4f58831e804b973f2de790557afa`. Completes the lifecycle coordination missed by get-bb#2773. Tagging @slopcop for re-review of head `0ab3801d24d4d8c04e9cc1bff808357cb0f07d70`. > AGENT GENERATED
## Human comments
## What was wrong
The compact New Thread page placed a capped three-row Recent list below
the composer in one page scroller. The composer moved away from the
thumb, the list looked complete even when more work existed, and rows
omitted provider, hierarchy, workspace, branch, and activity context.
## What changed
- Anchor the production composer at the bottom while realistic recents
scroll behind it.
- Leave the recents viewport on native overflow behavior; the
imperceptible product-managed scrollbar timer and state were removed.
- Keep the `Recent` label sticky and leave a partial row visible to
signal more content.
- Render every active thread with production provider marks, hierarchy,
status, workspace, branch, and attention-time metadata.
- Vertically center each provider tile against its thread title and
metadata block.
- Preserve highlighted descendants by expanding collapsed ancestors.
- Render the real production compact-home, recents, prompt box, and
overflow-fade components in Ladle with populated fixtures.
No compact drawer, panel, route-feedback, or server-cache behavior
changes in this layer.
## How you verified
- Original geometry and alignment evidence used Chrome for Testing
151.0.7922.71 at 393×852 CSS px, DPR 2, against exact base
`f4bbc2fe81a9b7639ff9a7396e172bddd89109e4` and feature head
`df613809d7080cbc145913355958478de5733245`. The current exact head is
`0eb3bef25c3589f2b3f40cdcb80b20cff131512a`.
- Measured the production compact-home subtree itself: 393×852;
production composer subtree: y=672, h=180; scroll viewport: y=318,
h=534; 23 production recent rows rendered.
- Measured provider-tile center alignment on representative parent,
child, and ordinary rows: the prior head placed every tile 12px above
its title/metadata center; the final head measures 0px center delta
before and after client navigation.
- Hard reload and thread navigation back to the compact home preserved
the alignment; no console errors.
- Scrolled that viewport to 360px and measured the `Recent` heading at
y=318, exactly pinned to the viewport's top edge.
- On the exact current head, the viewport has no product scroll
listener, idle timeout, `data-scrollbar-scrolling` state, or
`transient-scrollbar` class; the browser owns scrollbar presentation.
- Verified collapsed parents whose only activity came from a child: the
glyph and accessible label report the same aggregated state. Child-only
pending, plan, runtime, and unsubmitted-draft states have regression
coverage.
- On the exact current head, a realistic hidden child draft produced
`Thread has unsubmitted draft` on both the parent link and production
status glyph; the child stayed collapsed and the recents subtree
contained zero nested interactive pairs.
- The hierarchy toggle is now a sibling of the thread link; the
production recents subtree contained zero nested `a button` or `button
a` pairs.
- Compact-home geometry now uses `useLayoutEffect`, not `useEffect`.
Across 143 requestAnimationFrame samples from first real content, the
scroll viewport stayed at y=310 with no post-paint geometry jump.
- Remote CI passed all required tests, checks, package smokes, and
version gates on the exact final head. No local CI-equivalent command
was run.
<table>
<tr>
<th>Before — <code>main</code></th>
<th>After — compact production home</th>
</tr>
<tr>
<td><img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/5904f796578a5112b80ec007db7176bfd8fb83d4/layer1-before.svg"
width="393" alt="Before: main compact home" /></td>
<td><img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/234f625398536ff1b4dc4af74c1027698d99cfa0/pr2758-after-scrollbar-removal.svg"
width="393" alt="Exact final head: compact production home after
removing imperceptible scrollbar bookkeeping" /></td>
</tr>
<tr>
<th colspan="2">After — sticky header while scrolled</th>
</tr>
<tr>
<td colspan="2" align="center"><img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/f3574205a70ba781f588d9ac17fc9fd703a34610/pr2758-sticky.svg"
width="393" alt="After: Recent header remains sticky after scrolling"
/></td>
</tr>
</table>
### Child-only draft aggregation
| Feature head `62c4c6d40` — collapsed parent reflects its hidden child
draft |
| --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/9add1f52c7d662561b0f93eba3f8733d0574835a/pr2758-child-draft-final-head.svg"
width="393" alt="Feature head: production compact recents show a draft
glyph on a collapsed parent whose hidden child owns the draft" /> |
BB-Thread-ID: thr_wftu7bh9ez
> AGENT GENERATED
## Human comments ## What was wrong Deferred route navigation could appear inert, and a cold thread route could begin without metadata even though the persisted sidebar bootstrap already contained a usable thread placeholder. ## What changed - Show route-progress feedback only after a 120ms delay, then keep it visible for at least 320ms so quick routes do not flash and slow routes do not feel ignored. - Seed cold thread queries from the in-memory or persisted sidebar bootstrap before the full response arrives. - Keep indicator colors derived from the active theme anchors. No mobile-home layout, compact shelf, provider-discovery, or right-panel presentation changes in this layer. ## How you verified - The original behavior capture used Chrome for Testing 151.0.7922.71 at 393×852 CSS px, DPR 2, against exact feature revisions `e1b749660fb19ecc402799cfbbaa1cd887ac0188` and `ac97a215cb1db1640f11a2de60e9bf2b97d3a70a`. The direct layer remains unchanged at current exact base `0eb3bef25c3589f2b3f40cdcb80b20cff131512a` and exact head `1b8c66d38227299d70e3033bac8a2f2af35c294d`. - Held the real thread/environment requests pending, clicked the same production recent-thread anchor, and captured at 220ms. The base route had only an anonymous skeleton; the head had already reconstructed the real `Wire up automations CLI` header and production follow-up composer from persisted sidebar data. - Assertions and captures targeted the routed header/composer subtrees, not `document.body.innerText`. - Remote CI passed the timing tests, typecheck, lint, package smokes, and version gates on the exact final head. No local CI-equivalent command was run. | Before — no cold-route identity | After — immediate production placeholder | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/d2ad95502b764fe32cef1e834a34838db73036f6/pr2-before-nav.svg" width="393" alt="Before: anonymous cold route skeleton" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/90c46b9e1fad5c7546b259c0033a1f971fd5e6d3/pr2-after-nav.svg" width="393" alt="After: real title and production composer while the route request remains pending" /> | BB-Thread-ID: thr_wftu7bh9ez > AGENT GENERATED
## Human comments
## What was wrong
Listing providers repeatedly spawned provider-bridge health probes that
can take seconds per host, and provider logos used mutable URLs even
though the bytes were already hashed. The app also discarded a persisted
provider list that could avoid generic-icon churn during startup.
## What changed
- Cache provider installed-state per host and provider for five minutes,
invalidated by provider registration changes or failed probes.
- Reuse the persisted provider list while the live query settles.
- Add the icon content hash to provider registrations and immutable logo
URLs.
- Update plugin-provider registration and server tests for the explicit
hash contract.
No route-feedback, compact-home layout, shelf, or full-page panel
behavior changes in this layer.
## How you verified
- The original rendering capture used Chrome for Testing 151.0.7922.71
at 393×852 CSS px, DPR 2, against exact feature revisions
`ac97a215cb1db1640f11a2de60e9bf2b97d3a70a` and
`f65de938732b45cf993ed9fc721264229e0d96b3`. The scoped cache-ownership
fix was verified at exact base
`f906c20416a6ca5e508901c916597bfeaa114fdb` and then-head
`4b8a22b0aaada44f118eac54af996db0844d267c`; the final slimming-only
layer is exact base `1b8c66d38227299d70e3033bac8a2f2af35c294d` and exact
head `38c122fc1b74ae4a0afe28596f86e6da9a73aa48`.
- Primed the same persisted provider cache, held only the live
provider-list request pending, let immutable logo requests proceed, then
captured after the production recents subtree had rendered. The base
left visible provider cells blank; the head reconstructed the real
provider marks immediately from persisted data.
- Captures waited for the production recents subtree and its rows; no
page-body text assertion was used.
- Targeted server coverage holds one failing 502/504 health probe across
concurrent callers and verifies that both callers take the same fallback
path.
- Provider health ownership now uses explicit `{ hostId, providerId }`
keys. A failed probe invalidates only that host-provider entry, provider
registration can invalidate one provider across hosts, and settings
changes clear the complete cache. Regression coverage keeps a healthy
host cached after a different host fails.
- Targeted app coverage verifies capability-filtered placeholders omit
persisted providers that cannot satisfy maintenance.
- Remote CI passed server/app tests, typecheck, lint, package smokes,
and version gates on the exact final head. No local CI-equivalent
command was run.
| Before — live list pending | After — persisted production marks |
| --- | --- |
| <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/47dbd0714d5a290c33d608248379592cfdd9510f/pr3-before-provider.svg"
width="393" alt="Before: provider cells remain blank while provider
discovery is pending" /> | <img
src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/7bd06c492f30d1e67a9fd7d9b78f950c12b1cfa7/pr3-after-provider.svg"
width="393" alt="After: real provider marks render from the persisted
provider list" /> |
BB-Thread-ID: thr_wftu7bh9ez
> AGENT GENERATED
## Human comments ## What was wrong On compact viewports, the left navigation covered and dimmed the page, while the right panel rose as a dimmed bottom drawer. Both overlays hid the surface users were navigating from and made the product feel like disconnected modal sheets. ## What changed - Keep the left navigation mounted beneath the page and translate the live page right to reveal a 76vw shelf without a scrim. - Replace the right-panel bottom drawer with a 76vw right shelf that translates the live page left. - Keep the translated center pane square and let each shelf own one stable divider. - Preserve swipe, dismissal, deferred realization, desktop behavior, and compact panel state. - Use the right-panel glyph consistently now that compact presentation is no longer a bottom drawer. - Suppress the translated page-header actions and the fixed left-sidebar trigger while the right shelf is open, leaving the shelf-owned collapse control as the only visible sidebar control. - Add real many-tab shelf coverage in the production `ThreadSecondaryPanel` story. No full-page tool-tab behavior is introduced in this layer; every right-panel tab still uses the shelf. ## How you verified - Original shelf-geometry evidence used Chrome for Testing 151.0.7922.71 at 393×852 CSS px against exact base `f65de938732b45cf993ed9fc721264229e0d96b3` and then-head `991070eab2c60f00fdb242403b38890dffdc852d`. - Current compact-shelf evidence uses the same populated production Info route, realistic environment-backed fixture, open-shelf interaction state, and 393×852 viewport at capture base `4b8a22b0aaada44f118eac54af996db0844d267c` and capture head `108402ee09e4a70b2df61542e7054ce73fae924a`. The final square-pane layer is exact base `38c122fc1b74ae4a0afe28596f86e6da9a73aa48` and exact head `8bd201435a25ce4feb3579a5fe3b9fa84d22e4b6`. - Left: base drawer measured x=0, w=320 with an 80%-opaque backdrop and an unshifted app; head shelf measured w=298.67 with the live app shifted to x=298.68 and a transparent backdrop. - Right: base bottom drawer measured x=0, y=68.17, w=393, h=783.83; head shelf measured x=94.33, y=0, w=298.67, h=852 with the live app shifted to x=-298.68. - At the exact final head, both translated center panes measured 0px on all four corners, no shadow, and visible overflow. The left shelf owns a 1px right divider; the right Info shelf owns a 1px left divider. - The left capture found 23 real thread rows in the production sidebar subtree; the right capture found the production `Start terminal` action. Both captured subtrees had zero loading skeletons. - Exercised first paint through settled content, real client navigation, a hard reload, and a complete shelf transition with no browser errors. - Focus entered the dialog when opened; Tab and Shift+Tab stayed contained; a nested production tooltip consumed the first Escape; the second Escape closed the shelf and restored the exact right-panel trigger. - The exact-head Info shelf measured 296.4×844 at x=93.6 in the 390×844 evidence viewport. Translated header actions and the fixed left trigger were absent while the panel-owned collapse control remained visible. - At the exact current head, the open Info shelf exposed one on-screen sidebar control: `Hide right panel` in `thread-secondary-panel-top-chrome`. The fixed left trigger was absent from the DOM and focus order; closing the panel restored it, and opening the left shelf kept the right shelf closed. - Remote CI passed tests, typecheck, lint, package smokes, and version gates on the exact final head. No local CI-equivalent command was run. ### Left navigation: modal drawer → persistent shelf | Before | After | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/a20e0040a9e60ea8b22349c40335ca1a9adf5c6b/pr4-before-left.svg" width="393" alt="Before: left modal drawer covers and dims the app" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/ce6a6c5de38d20aa33013c514085f1ca13bb20bc/pr4-after-left.svg" width="393" alt="After: left persistent shelf reveals a rounded, clipped center pane without a scrim" /> | ### Right panel: bottom drawer → persistent right shelf | Before | After | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/942305656055bd9b3ae10a76cd5237767ca4e7dd/pr4-before-right.svg" width="393" alt="Before: right panel is a full-width bottom drawer" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/6d9cb9a739fbef47d6d9167d4db8af2b09403969/pr4-after-right.svg" width="393" alt="After: right shelf reveals a rounded, clipped center pane" /> | ### Square shelf edges before the toggle-ownership fix The earlier before/after captures document the drawer-to-shelf change at their labeled capture heads. The square-pane captures below come from the preceding head; the final toggle-only update leaves this geometry unchanged. | Left shelf — square pane | Right Info shelf — square pane | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/45ddcf245aaf3dd2f441c3b9a21d38cb96f5dc6b/left-square-pane.svg" width="195" alt=\"Preceding PR head: production left shelf with a square center pane and one stable divider\" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/520f7ffeb3c06c2a6c41cbb3e9dcadf7327253b6/right-square-pane.svg" width="195" alt=\"Preceding PR head: production Info shelf with a square center pane and one stable divider\" /> | ### One compact sidebar control at a time The preceding get-bb#2761 head correctly suppressed translated workspace actions but still showed the fixed left trigger beside the right shelf owned control. At the exact final head, the fixed trigger leaves the DOM and focus order while the right shelf is active. Closing the right shelf restores it; opening the left shelf keeps the right shelf closed and leaves only the left trigger on screen. | Before — two competing controls | After — right shelf owns one control | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/520f7ffeb3c06c2a6c41cbb3e9dcadf7327253b6/right-square-pane.svg" width="195" alt="Preceding PR head: the fixed left trigger competes with the right shelf collapse control" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/37ffb2fde812f68e2ec1b90d1f514b7c06460660/right-panel-single-toggle.svg" width="195" alt="Exact final PR head: the production Info shelf exposes only its own collapse control" /> | BB-Thread-ID: thr_wftu7bh9ez > AGENT GENERATED
## Human comments ## What was wrong After the right panel became a shelf, every tab remained constrained to 298.67px. Selecting New Tab could update the tab strip while leaving Info content visible, so tool surfaces did not receive a usable phone-width canvas. A newly created terminal could also lose its persisted active selection while the terminal list caught up, causing the settled tab to fall back to the shelf. ## What changed - Let the active tab determine the compact presentation; the control that opened the panel does not independently choose shelf versus full-page. - Keep Info in the compact right shelf. - Expand every non-Info tab, including New Tab and tabs opened from it, from the right to a full-width surface above fixed app chrome. - Render the available production tab and preserve its full-page presentation while persisted active selection catches up. - Select and render New Tab's production content after its loading state settles. - Close the right panel when the left navigation opens so compact surfaces cannot compete for the page. - Replace substitute story markup and wrappers with the production shelf, panel, file preview, compact home, sidebar provider, and sidebar inset. ### Compact presentation rules The opener only selects or restores a tab. The active tab kind determines the panel geometry, and switching tabs changes that geometry immediately. | Trigger or active destination | Compact result | | --- | --- | | Open or select Info (`thread-info`) | Right shelf at 76vw. An unresolved or empty initial tab state also uses this safe fallback. | | Open or select New Tab | Full-page at 100vw. | | Open or select Terminal, Diff, Browser, file preview, plugin page, or any other non-Info tab | Full-page at 100vw. | | Tap the right-panel toggle on an existing thread | Reopen the current tab: Info returns as a shelf; a non-Info tab returns full-page. If no tab has resolved, Info is the default shelf destination. | | Tap the right-panel toggle on Home / new-thread compose | Create and select New Tab, so the panel opens full-page immediately. | | Open the left navigation or close the right panel | Hide the right surface; this does not create a competing shelf. | No mobile-home, navigation-feedback, provider-cache, or base shelf behavior changes in this layer. ## How you verified - Chrome for Testing 151.0.7922.71 at 393×852 CSS px, DPR 2, against capture base `108402ee09e4a70b2df61542e7054ce73fae924a` and capture head `fb9902ab803b381574252195d4980b937d9405c4`. The final layer is exact base `8bd201435a25ce4feb3579a5fe3b9fa84d22e4b6` and exact head `0f50516ccab03168a1730e62d05c65b512405571`; it inherits the square-pane rollback without changing this layer’s full-page tab policy. - From the same production New Tab surface, chose `Start terminal` and waited for the real `zsh` tab and xterm subtree to settle with zero panel skeletons. - Base: terminal remained in the 298.67px shelf at x=94.33 and the app stopped at x=-298.68. Head: the same terminal became a 393px full-page surface at x=0 and shifted the app completely to x=-393. - Reproduced the active-selection race after terminal creation, then verified the final head keeps the settled production terminal full-page with no browser errors. - Rechecked the capture head in the branch web app on `/projects/proj_m65y3vujaj/threads/thr_done`, using one realistic environment-backed fixture, the same persisted `AppPageHeader.tsx` tab, the same populated Recent item, and the same viewport for both current-head captures. Only the active tab changed. - Waited for production content before capture: Info rendered environment, branch, merge base, PR, commits, and changed files; New Tab rendered file search, production actions, and a Recent file opened through the production panel flow. - Measured each production right-panel dialog subtree in the populated capture: Info settled at x=94.33, 298.67×852; New Tab settled at x=0, 393×852. The exact final-head control capture measured Info at 296.4×844 and New Tab at 390×844. - In the Info shelf, translated center-pane actions and the fixed left trigger were absent while the panel-owned `Hide right panel` control remained visible. - At the exact final head, Info exposed one on-screen sidebar control, `Hide right panel`, with the fixed left trigger absent from the DOM and focus order. New Tab preserved the same one-control ownership while full-page. Closing either right surface restored the left trigger; opening the left shelf left only that trigger on screen and kept the right surface closed. - Focus entered the panel, Tab and Shift+Tab stayed contained, the first Escape dismissed the nested tooltip without closing the tool page, and the second restored the exact right-panel trigger. The exact final-head run found zero focusable fixed-left triggers while either right surface was active. - The exact final-head Chrome pass had no runtime exceptions. Its only failed response was the deterministic no-environment fixture returning 409 for `thread-storage/files`; the rendered production empty states and panel interactions remained valid. Remote CI passed tests, typecheck, lint, both package smokes, and version gates on the exact final head. No local CI-equivalent command was run. ### Populated feature evidence at the preceding head These captures retain the realistic populated fixture from the preceding feature head. They prove the Info shelf exception and the New Tab full-page policy with production content. | Info — 76vw shelf | New Tab — full page | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/b0fd7ce7a815d56e686651b6b7b4d4da2d2cef97/pr2762-info-current.svg" width="260" alt="Preceding feature head: populated production Info tab renders in the compact right shelf" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/b014aec0871e11a932a061dbd65a10014cc2d631/pr2762-new-tab-current.svg" width="260" alt="Preceding feature head: production New Tab renders file search, actions, and realistic Recent files full page" /> | ### Exact final head: one owning control Chrome for Testing 151.0.7922.71 captured both states in one session on `/projects/proj_m65y3vujaj/threads/thr_done` at 390×844 CSS px. The route, fixture, viewport, and open right-panel interaction stayed fixed; only the active Info versus New Tab destination changed. Both are production panel surfaces, and neither capture contains a loading skeleton. | Info — shelf-owned collapse | New Tab — full-page collapse | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/37ffb2fde812f68e2ec1b90d1f514b7c06460660/pr2762-info-single-toggle.svg" width="195" alt="Exact final PR head: production Info shelf shows only its own collapse control" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/4745fc94cdd9099a3a204446764818a5284dceab/pr2762-new-tab-single-toggle.svg" width="195" alt="Exact final PR head: production New Tab full page shows only its own collapse control" /> | ### Original base/head width regression This historical pair remains the original feature regression; the current-head pair above is the final-branch evidence. | Before — tool tab constrained to shelf | After — same tool tab full page | | --- | --- | | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/e0665b43f4153ad50636b954ab02a8ff69137ef4/pr5-before-newtab.svg" width="393" alt="Before: production zsh tab remains constrained to the rounded right shelf" /> | <img src="https://gist.githubusercontent.com/brsbl/d8363c420a5e5c2167498079a27162f0/raw/ad6730e8e37be548bc2f94558169d57e32724a23/pr5-after-newtab.svg" width="393" alt="After: production zsh tab remains selected and expands to a full-page compact surface" /> | BB-Thread-ID: thr_wftu7bh9ez > AGENT GENERATED
## Human comments ## What was wrong File-preview tabs enlarge their close target to 36px on narrow coarse-pointer screens, but the selectable tab trigger retained its 6px desktop left inset. Centering the close glyph in that wider target moved it over the filename: the glyph ended at x=122 while the label began at x=120, producing a 2px overlap. ## What changed - Add a coarse-pointer left inset to non-icon tab triggers only when they have a close action with the enlarged touch target. - Preserve the 36×36px close target while restoring 6px of visible space before the filename. - Add focused regression coverage for the conditional file-tab spacing. - No wire, CLI, guide, or documentation behavior changed. ## How you verified - Dev Browser at a 480×700 viewport with coarse-pointer emulation: the close-glyph-to-label gap changed from -2px to 6px while the close target remained 36×36px. - The focused regression test failed before the fix and passed afterward. - Post-rebase: `pnpm exec turbo run test --filter=@bb/app --force -- --run src/components/ui/tab-pill.test.tsx` (5 tests passed). - Post-rebase: `pnpm exec turbo run typecheck --filter=@bb/app --force`. - Full app verification before the no-op rebase: 456 test files passed, 3,643 tests passed, 4 skipped. BB-Thread-ID: thr_q2ws2irggv > AGENT GENERATED
…n checkpoint (get-bb#2779) ## Human comments ## What was wrong bb had no way to put anything between "a user asked for work" and "bb runs it": no scheduled sends, no concurrency control, and four parallel single-purpose parking mechanisms (`dispatch_holds` never existed on main, but `deferred_thread_messages`, the reprovision-parked turn, and the drain-only queue each solved one blocked-message case with its own machinery). Rate-limit retries were faked by `plugins/provider-retry` re-reading the whole event log (~450 lines) and injecting a synthetic "Please continue." message, so the provider never saw the same conversation twice. There was no plugin surface that could defer, refuse, or reschedule a dispatch. ## What changed **The model: a send is always a dispatch attempt.** If nothing blocks it, it dispatches directly — the happy path is byte-for-byte unchanged and allocates nothing (test-guarded: a stock install registers zero hooks). If something blocks it, the message queues as a row carrying a typed `waitingOn` (`time` | `thread-busy` | `provisioning` | `interaction` | `host-offline` | `plugin`), and core re-attempts when conditions change (due sweep, thread-idle drain, workspace-ready, interaction-settled, plugin `recheck`, Send-now, orphan sweep). `deferred_thread_messages` is deleted; its cases are queue rows. Threads gain a canonical `pending` status (created, first message never dispatched) — scheduled spawns do zero provisioning until due. **Plugin API** (all `experimental_`): `bb.experimental_hooks.on("message.dispatch", h)` — one checkpoint, run identically for fresh sends, drained rows, and steers, returning `proceed | wait(reason, sendAt?) | reject(message)`; fail-closed (10s box, plugin named). `bb.experimental_hooks.recheck(hook)` asks core to re-pose the question. New `bb.events`: `message.queued`, `message.dispatched`, `turn.failed` (ids + failure facts). Public API: `sendAt` on send/create, `threads.retry({turnRequestId?, sendAt?})` (re-dispatch decided by the provider's acceptance record: an input the provider never accepted is re-sent verbatim, an accepted turn is continued with an agent-only nudge; the original execution tuple is replayed and no user message is duplicated), `threads.listRunning()` (exact for cold admissions inside a hook — a `pending → starting` flip commits before the evaluation lock releases; a warm follow-up's `idle → active` flip lands in the send transaction just after it, so a burst of follow-ups to distinct idle threads can briefly under-report), `threads.count()`. **Plugins**: new `scheduled-send` ("Send later…" via the composer's own submit pipeline + dialog; **off by default**) and `concurrency-limit` (global + per-host caps as one pure hook over `listRunning`; **off by default**); `provider-retry` rewritten to a `turn.failed` listener calling `threads.retry` — `recovery.ts` and the synthetic continue are deleted (plugin: 1,427 → ~300 lines). **UI/CLI**: queued rows render their wait (kind icon, reason, live countdown, retry attempt, failure reason) with Send-now/Edit/Cancel; sidebar shows a clock for threads with waiting work and the error glyph when a queued dispatch failed; `bb thread queue` gains wait columns; new `--send-at` on spawn/tell, `bb thread count`, `bb thread retry`; guide + skills updated alongside. **Wire**: `HOST_DAEMON_PROTOCOL_VERSION` → 176 (thread-event schema changes ride the daemon event batch). One drizzle migration (`0110`) on main's chain: queue wait columns + partial indexes, `threads.pending_start_context`, and the `deferred_thread_messages` drop. Plugin SDK 0.4.31 with `docs/api_to_audit.md` entries for every new `experimental_` member. ## How you verified Repo-wide `turbo run typecheck` (82/82) and `turbo run lint --filter=@bb/app` (0 errors) at every layer; final battery across 15 packages (~7,900 tests: server 2094, app 453 files, cli, db incl. migration-replay + `EXPLAIN QUERY PLAN` index pins, domain, contracts, plugin-sdk, thread-view, client-core, three plugins, templates, plugin-api-map api-sync). Exit-criteria tests: `--send-at` survives restarts; exactly-once dispatch under racing drains; orphaned plugin waits clear; retries re-issue the original turn with no duplicated message; hook exactness (attempt N+1's `listRunning` sees attempt N's admission — load-bearing test fails if the commit moves outside the lock); zero-hook installs byte-identical. Integration suite: failures on the loaded build machine were verified as environment flakes — the same suite on a clean origin/main worktree failed 4× more files under identical load; all failing files pass in isolation. > AGENT GENERATED --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Human comments ## What was wrong Top-level sidebar sections rendered collapsed child activity as an absolutely positioned sibling of the section actions. The indicator only switched into normal flow when both the compact breakpoint and a coarse pointer matched, so a mobile-width drawer with a fine or remote pointer kept the desktop overlay and allowed the activity glyph to collide with the trailing controls. ## What changed The collapsed activity indicator now lives inside a stable, relatively positioned trailing-controls container. At compact widths it becomes an in-flow status cell regardless of pointer type; at desktop widths it remains pinned to the trailing edge and keeps the existing hover fade behavior. A regression test covers a collapsed working section with three visible actions. This is UI-only: there are no wire, CLI, guide, or documentation changes. ## How you verified - `pnpm exec turbo run test --filter=@bb/app -- src/components/sidebar/ProjectListSectionHeader.test.tsx` (8 tests pass; the new containment assertion fails before the fix) - `pnpm exec turbo run typecheck --filter=@bb/app` - `pnpm exec turbo run lint --filter=@bb/app` (0 errors) - `dev-browser` before/after capture at a 376px compact viewport with a fine pointer - `git diff --check origin/main...HEAD` > AGENT GENERATED
## Human comments ## What was wrong Compact right-panel visibility is owned separately from persisted tab state. Closing the final tab emptied and closed the tab model, but the compact drawer stayed visible and rendered the unavailable-view fallback. ## What changed The shared file-tab and terminal-tab close paths now notify their panel host when a close removes the final tab. Root compose, thread detail, and plugin panel hosts use that notification to close compact drawer visibility immediately. A focused compact-viewport regression test verifies that closing the remaining tab restores the panel trigger without showing the unavailable fallback. There are no wire, CLI, guide, or public API changes. ## How you verified - Confirmed the regression test failed twice before the fix with This panel view is unavailable. - pnpm exec turbo run test --filter=@bb/app -- src/components/plugin/PluginPanelRightPanelHost.test.tsx src/components/secondary-panel/useThreadFileTabs.test.ts src/lib/fixed-panel-tabs.plugin-terminal.test.ts (50 tests passed) - pnpm exec turbo run typecheck lint --filter=@bb/app (successful; lint reported 0 errors) - Rebased cleanly onto origin/main at 46f23c4 before final verification. > AGENT GENERATED
## Human comments ## What was wrong Provider discovery correctly cached installed state for five minutes, but the successful provider-installation route did not invalidate an earlier negative answer. After installing an installed-only plugin provider, the provider could therefore remain hidden until the cache expired. This was the remaining correctness gap from [the provider discovery caching change](get-bb#2760). ## What changed - Invalidate only the installed-state entry for the target host and provider when installation events contain a successful completion. - Add a public-route regression that caches the provider as absent, installs it successfully, then verifies the next provider listing probes again and includes it. - Preserve discovery caching, retries, persisted provider roster behavior, and immutable logo URLs. There are no server/daemon wire changes, CLI changes, or documentation changes. ## How you verified - Confirmed the regression failed before the fix: installation returned HTTP 200 with `success:true`, but the installed-only provider remained absent and was not probed again. - `pnpm exec turbo run test --filter=@bb/server -- --run test/public/public-provider-installations.test.ts test/system/execution-options.test.ts test/providers/provider-registry.test.ts` — 3 files, 61 tests passed. - `pnpm exec turbo run typecheck --filter=@bb/server` — passed. - `git diff --check origin/main...HEAD` — passed after rebasing onto current `origin/main`. > AGENT GENERATED
## Human comments ## What was wrong PR get-bb#2665 turned the follow-up shortcut preference into a Queue/Steer picker, but Queue stayed the default. Steer gives a better first experience. A simple default change would also affect existing stores. `getAppSettings` starts from `defaultAppSettings` and overlays only keys with an `app_settings_values` row. A user who never opened Settings has no preference row. ## What changed - `packages/db/drizzle/0112_steer_on_enter_default.sql` writes `false` for an existing store without an explicit preference. A non-personal project, a thread, or a saved setting shows prior use. Migration 0006 seeds `proj_personal` into every store, so that project alone does not show prior use. - `packages/domain/src/app-settings.ts` now defaults `steerActiveThreadOnEnter` to `true`. A new install receives Steer because the migration leaves its empty store unchanged. - The configuration guide, the BB CLI skill reference, and the guide template explain the split default. The migration cannot identify an earlier install that has only the seeded personal project. That store receives Steer because it has no durable prior-use marker. This change does not alter a schema, a route, or the host-daemon wire contract. `HOST_DAEMON_PROTOCOL_VERSION` does not change. ## How you verified - `pnpm exec turbo run test typecheck --filter=@bb/db --filter=@bb/domain --filter=@bb/templates --filter=@bb/server --force` passed after the rebase. Turbo completed 14 tasks. The suites passed 2,778 tests and skipped one expected server file. - Five database cases cover an existing store, a personal thread, a seeded-only store, a full migration run, and an explicit preference. - The EAP codename scan found no match in the working tree, `HEAD`, added lines, or commit messages. Fixes # > AGENT GENERATED Co-authored-by: Sawyer Hood <kirbyhood@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Human comments ## What was wrong The compact left sidebar tracked closing gestures only from inside its panel, while the backdrop covering the exposed main-content strip handled clicks only. The compact right shelf had no swipe-dismiss controller after its shelf conversion, so neither its panel nor its exposed-content dismiss layer could track a closing swipe. A dismiss controller without an edge reservation would also compete with browser history navigation gestures. ## What changed Added one shared horizontal-dismiss hook for pointer/touch tracking, intent and velocity arbitration, nested horizontal-scroll protection, settling, and cleanup. The left and right shelf adapters now attach it to both the panel and their exposed-content dismiss layer, configure their direction and visual transforms, and reserve the corresponding 24px browser-navigation edge. No wire, CLI, guide, or protocol changes. ## How you verified - Added red-to-green regressions for closing both shelves from the remaining exposed-content strip. - Added red-to-green regressions proving both corresponding 24px browser edges are ignored. - pnpm exec turbo run test --filter=@bb/app -- --run src/components/ui/sidebar.test.tsx src/components/secondary-panel/CompactSecondaryPanelShelf.test.tsx src/components/secondary-panel/SecondaryPanelLayout.test.tsx (56 passed) - pnpm exec turbo run typecheck --filter=@bb/app - pnpm exec turbo run lint --filter=@bb/app (0 errors) - Manually verified the left exposed-strip gesture at a 390x844 Chromium viewport before shutting down the requested dev share. Reported directly; no linked GitHub issue. > AGENT GENERATED
## Human comments ## What was wrong Every branch bb creates for a managed worktree or a new checkout branch was hardcoded to `bb/<slug>-<threadId>` in `buildManagedBranchName`. Nothing consumed `bb/` as a marker; it was simply a literal with no way to change it. Users who share a repository with teammates, or who want their agent branches under their own namespace, were stuck with it. ## What changed **Setting.** `packages/domain/src/app-settings.ts` adds `managedBranchPrefix`, default `bb/`. `managedBranchPrefixSchema` validates a candidate by synthesizing a branch name from it and running the existing `isValidGitBranchName`, which is now exported from `git-checkout.ts`. Max length 64. `app_settings_values` is a key/value table, so there is no migration; the key falls back to `defaultAppSettings`. **Server.** `buildManagedBranchName` takes an explicit `branchPrefix` instead of the literal. The three call sites (`thread-provisioning-environment.ts` ×2, `environment-provisioning-internal.ts`) resolve it once from `getAppSettings(deps.db)` at the boundary and pass the explicit value down through the provisioning plan args. The prefix does not need a trailing slash: `sawyer/wt-` gives `sawyer/wt-<slug>-<threadId>`, and an empty prefix gives `<slug>-<threadId>`. A new prefix applies to branches bb creates after the change; it renames nothing. **UI.** A text field in Settings → General that commits on Enter or blur, reverts on Escape, previews the branch name your prefix produces, and marks an invalid prefix with a destructive border plus a `role="alert"` message. Empty shows a `No prefix` placeholder. **Agent surfaces.** `bb settings general managedBranchPrefix <prefix>` and `sdk.system.updateGeneralSettings` both work through the existing generic settings surface with no new code. Documented in `docs/configuration.md`, the bb-cli skill reference `app-settings.md`, and the customization guide chapter. No `HOST_DAEMON_PROTOCOL_VERSION` bump: `branchName` in `environment.provision` already carried an arbitrary branch string, and its type, requiredness, and meaning are unchanged. `AppSettings` is a published SDK type, so the new field changes `bundled-types/bb-plugin-sdk.d.ts` and `packages/plugin-api-map/sdk-public-api.json` is regenerated to match; main's own bump to `@get-bb/plugin-sdk` 0.4.35 is unpublished, so it carries this change too and no further bump is needed. ## How you verified **Tests added** (each fails against the hardcoded literal, passes now): - `packages/domain/test/app-settings.test.ts` — accepted and rejected prefixes, default round trip. - `apps/app/src/views/SettingsView.branch-prefix.test.tsx` — commit on Enter, empty prefix, invalid revert on blur, Escape. - `apps/server/test/threads/thread-create-helpers.test.ts` — configured prefix on both branch-name shapes, empty prefix. - `apps/server/test/threads/generated-branch-names.test.ts` — sets the setting, then asserts the queued `environment.provision` command carries `sawyer/wt-custom-prefix-branch-<threadId>`. **Commands.** `pnpm exec turbo run typecheck` 83/83. `pnpm exec turbo run test` 80/80. `node packages/plugin-sdk/scripts/check-npm-version-guard.mjs` passes at 0.4.35. **Live end-to-end** against the dev app, with a scratch git repo as the project: | Prefix set | Branch git actually created | | --- | --- | | `sawyer/wt-` | `sawyer/wt-add-a-greeting-helper-thr_ffm22h8kku` | | `""` (empty) | `no-prefix-at-all-thr_tstep2ffyh` | `git worktree list` confirmed a real checkout on the prefixed branch. Also verified the value round-trips through the UI and survives a reload, `/api/v1/system/config` reports it, `bb settings general managedBranchPrefix ""` works, and the CLI rejects `bad prefix/` with a named error. The live run caught two UI defects the unit tests did not, both fixed here: the invalid state had no visual signal (the shared `Input` has no `aria-invalid` styling), and the empty-field placeholder showed `bb/`, which reads as though the field holds `bb/` when empty means no prefix. > AGENT GENERATED
## Human comments ## What was wrong The agent-tool boundary detected a plugin-owned Zod schema through `safeParse`. It then used the host Zod converter. That converter read private data from another package instance. See the [reproduction report](https://get-bb.github.io/reports/issues/2837.html). ## What changed The shared plugin policy now uses the schema-owned JSON Schema converter when available. It keeps the host converter as a compatibility fallback. The production server and the fake plugin host use the same helper. This change has no wire or protocol change. ## How you verified The focused regression failed before the fix and passed after the fix. The final focused run passed 20 tests. The full server suite passed 2,121 tests. The Plugin SDK suite passed 221 tests. Turbo typechecks passed for `@bb/server` and `@get-bb/plugin-sdk`. The change has 67 added and deleted text lines across four files. Fixes get-bb#2837 > AGENT GENERATED Co-authored-by: Sawyer Hood <sawyerjhood@gmail.com>
## Human comments ## What was wrong Queued-message readiness was decided independently by startup events, idle sweeps, plugin/timer drains, failed-row handling, and manual Stop handling. Each fix could pass alone while their predicates disagreed when composed: a settled `turn-starting` row could be discovered but rejected, a failed grouped sibling could ride with a healthy row, and a stale runtime event or in-flight hook could cross a manual Stop. Startup admission also trusted a stale active thread after it changed to error, which could strand a row or change `steer` semantics. ## What changed - Made startup admission atomic: it either queues against the still-active startup or retries once from the current thread state. - Centralized automatic whole-group readiness across plugin, time, `thread-busy`, and `turn-starting` waits. - Replaced the positional `sendNow`/optional-callback claim API with an explicit `automatic` versus `explicit-send` policy. - Enforced failed-row exclusion and manual-Stop pause inside the group claim transaction. Explicit Send now remains the override; independent scheduled, plugin-held, and system-notice rows remain drainable. - Required a post-Stop client request before a later root turn can clear the pause, and rechecked the pause at final dispatch consumption. - Used one SQL `IN` predicate for idle-drainable wait kinds. This supersedes get-bb#2866 and get-bb#2867 and includes the failed-group and Stop-boundary follow-ups. There is no host-daemon wire, schema, CLI, SDK, migration, or documentation contract change. ## How you verified - Added deterministic regressions for the settled-startup lost wake, mixed-wait groups, failed grouped siblings, stale post-Stop starts, hook-held Stop races, and active-to-error startup admission for both `steer` modes. - Focused Turbo server matrix: 70/70 passed. - Focused Turbo DB/query-plan matrix: 68/68 passed. - Standalone production-path Stop race harness: both races passed. - Full DB suite: 444/444 passed. - Full server suite: 2,131 passed; 10 unrelated `install-machine-script` tests failed because their fake host daemon did not connect. - `pnpm exec turbo run typecheck --filter=@bb/server --filter=@bb/db` passed. - `pnpm exec turbo run build --filter=@bb/server` passed. - `git diff --check` passed. > AGENT GENERATED
## Human comments ## What was wrong The app-level toaster forwarded the desktop bottom-right position at every viewport size. On mobile, archive feedback therefore occupied the same lower-screen region as the home composer and obscured its controls. ## What changed - Override toast placement to top-center at the shared compact breakpoint. - Preserve the configured toast position on desktop. - Add focused assertions for compact top-center and desktop bottom-right behavior. - No wire, persistence, SDK, CLI, or daemon protocol behavior changed. ## Screenshots The comparison uses the same disposable thread, archive interaction, open home composer, light theme, and 390×844 viewport. The only meaningful difference is the code revision: the current merge base (`b4628ade6`) for before and the exact PR head (`1eca51a39`) for after. | Before — merge base b4628ad | After — PR head 1eca51a | | --- | --- | |  |  | ## How you verified - Chrome for Testing 151.0.7922.71 against the exact branch web app at 390×844. - Hard reloaded the seeded thread, archived it through the rendered action menu, followed the client navigation home, opened the composer, and observed the toast through its settled state. - On the merge base, confirmed the toast occupies y=760–828 over the composer's lower-screen controls. - On the exact PR head, confirmed the toast occupies y=16–84 while the composer begins at y=665, with no overlap or console errors. - Focused compact and desktop regression tests cover the responsive position behavior. - Remote CI passed on final head `1eca51a39`: 13 checks successful and 2 not-applicable checks skipped. - `git diff --check origin/main...HEAD` BB-Thread-ID: thr_mq2niw3uxf > AGENT GENERATED
## Human comments ## What was wrong The parent PR centers compact-viewport toasts at the top edge, where they cover the app's 48 px top navigation and its controls. The first implementation also encoded that 48 px chrome height separately in the toaster offset and shared chrome-row class, allowing those two surfaces to drift if the navigation height changed later. ## What changed - Inset compact toasts by the device top safe area, the shared app chrome row height, and the standard 16 px toast gap. - Define `--bb-app-chrome-row-height: 3rem` as the single length owner used by both chrome rows and the toast calculation. - Apply the inset to both Sonner's phone and wider compact layout modes while preserving configured offsets on the unaffected edges. - Assert the shared token declaration and both consumers in focused regression tests. - No wire, persistence, SDK, CLI, or daemon protocol behavior changed. ## Screenshots The comparison uses the same disposable thread, archive interaction, open home composer, light theme, and 390×844 viewport. The before image is the parent PR behavior; the after image is exact head `168db15df`. | Before — toast overlaps navigation | After — toast below navigation | | --- | --- | |  |  | ## How you verified - Remote CI passed on exact head `168db15df`; all applicable checks succeeded and the two not-applicable checks were skipped. - Chrome for Testing 151.0.7922.71 at 390×844 hard reloaded the seeded thread, archived it, navigated home, opened the composer, and completed the toast animation. - The exact-head archive toast rendered centered at y=64–132, below the shared 48 px chrome row plus 16 px gap, with the composer clear, no horizontal overflow, and no console or page errors. BB-Thread-ID: thr_mq2niw3uxf > AGENT GENERATED
## What was wrong The manually organized sidebar called its root thread group `Unorganized`, while the same content group is called `Threads` in the other sidebar views. The view-dependent name made one stable destination sound like an error state and created unnecessary sidebar terminology. ## What changed - Use `Threads` for the loose-thread group in every organization mode. - Update the remove-section confirmation to say that released threads move back to `Threads`. - Keep grouping, ordering, drag-and-drop, status, and thread behavior unchanged. No public or wire contracts changed. ## Screenshots Both screenshots use the same dev database, route, chronological organization mode, light theme, and 1440×900 viewport. ### Before — main (`b4628ade6`)  ### After — this PR (`551ccd018`)  ## How you verified - `git diff --check` - Chrome for Testing 151 at 1440×900 against the exact main and PR-head branch web apps. - Confirmed the same chronological sidebar fixture renders `Unorganized` on main and `Threads` on this PR without changing row order or content. - Remote CI passed the required checks, app/server/package/integration tests, and Linux/macOS package smoke on the current head; iOS simulator and Node Compatibility Smoke were intentionally skipped by workflow configuration. - Per repository policy, no CI-equivalent checks were run locally. Fixes # BB-Thread-ID: thr_ccffp4w2p2 > AGENT GENERATED
## Human comments ## What was wrong The side-chat footer chose between the pending-interaction banner and the entire `FollowUpPromptBox`, so showing a question or approval also removed the queue stack that lived inside the prompt box. After rendering both surfaces, the queue card's composer-attached `-mb-5` treatment caused a second bug: it overlapped and clipped the pending-interaction card because no composer sat beneath it. This is the root cause of get-bb#2869. Mobile side-chat sizing had a separate portal boundary bug. The visual-viewport hook published `--bb-shell-height` on the nested sidebar provider, but `CompactSecondaryPanelShelf` portals the compact side-chat drawer to `document.body`. The drawer therefore kept the full layout-viewport height while the keyboard reduced the visible viewport, leaving its prompt box below the screen. Side chats also bypassed the main follow-up submission guard. `buildSideChatSubmitMode` hardcoded both pending-interaction inputs to `false`, while `EmbeddedThreadChat` treated an unresolved or failed interactions query as an empty list. A composer could therefore appear usable before bb knew whether the child thread was waiting for input; a submission made during that window was held by the server even though the pending interaction and queue were not visible. ## What changed - Always render the side-chat `FollowUpPromptBox`, pass the pending interaction into its dedicated slot, and keep its queued-message stack mounted while the composer is hidden. - Treat queued messages as disabled while the interaction is pending. - Add an explicit `attachedToComposer` contract to `QueuedMessagesList`; side-chat queues use standalone card spacing during an interaction, while normal side-chat and main-thread queues retain the attached composer cap. - Pass real pending-interaction and initial-loading state through `buildSideChatSubmitMode`, matching the main follow-up submission guard. - Hide the side-chat composer behind a visible checking state until the first interaction request resolves. If it fails, keep the composer unavailable and show an inline Retry action. - Keep queues attached to the visible composer for plugin-owned interactions, which deliberately do not replace that composer. - Publish the mobile shell height and keyboard safe-area override on `document.body`, the common style ancestor for both the app shell and body-portaled compact drawers. - Add regression coverage for the combined question-and-queue state, standalone queue-card styling, unknown and failed interaction state, plugin-owned interactions with queues, and the real body-portaled side-chat shelf inheriting the corrected viewport height. - This is UI-only: there are no host-daemon wire, CLI, guide, or documentation changes, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. ## How you verified - Added focused submission-policy tests that failed before the fix because active child side chats still returned queue mode while pending-interaction state was loading or present, then passed once side chats reused the real guard. - Added component regressions that failed before the fix because an unresolved or failed interactions request exposed the composer; they now pass with a checking state and retryable error state. - Added the plugin-interaction-plus-queue regression requested in review; it failed before the fix because the queue used standalone styling above a visible composer, then passed after deriving placement from the composer-blocking condition. - `pnpm exec turbo run test --filter=@bb/client-core -- --run test/threadDetailPromptSubmission.test.ts` — 17 tests passed. - `pnpm exec turbo run test --filter=@bb/app -- --run src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx` — 15 tests passed. - `pnpm exec turbo run test --force --filter=@bb/app -- --run src/components/promptbox/banner/QueuedMessagesList.test.tsx src/components/thread/embedded-chat/EmbeddedThreadChat.test.tsx` — 56 tests passed before the latest focused additions. - `pnpm exec turbo run test --force --filter=@bb/app -- --run src/components/layout/useMobileVisualViewportHeight.test.tsx src/components/secondary-panel/CompactSecondaryPanelShelf.test.tsx src/components/layout/AppLayout.root-compose-project.test.tsx src/components/layout/AppLayout.sidebar-resize.test.tsx` — 32 tests passed. - `pnpm exec turbo run typecheck --filter=@bb/client-core --filter=@bb/app` — passed. - `pnpm exec turbo run lint --filter=@bb/client-core --filter=@bb/app` — passed with 0 errors and the existing warnings. - `pnpm exec turbo run build --filter=@bb/app` — passed with existing Vite/chunk warnings. - `git diff --check` and Prettier checks — passed. - Reproduced the interaction states with a real Claude Code provider question and a CLI-confirmed queued message, then checked desktop (1440 x 1000) and mobile (390 x 844) views. Live geometry changed from a 12 px overlap to the intended 8 px gap, and the normal composer states remained unchanged. - Confirmed the real QA sidechat still has the pending `user_question` at the server boundary while testing the fixed client policy. - Reproduced the Android IME behavior in a target-SDK-36 System WebView harness. WebViews that report a reduced visual viewport now pass that height through to the body-portaled shelf. A host that consumes IME insets exposes no web-side keyboard geometry and must correct its native inset propagation. Fixes get-bb#2869 > AGENT GENERATED
## Human comments ## What was wrong When saving a new managed worktree branch prefix failed, the Settings input continued displaying the rejected draft even though bb retained the previously saved value. Users could therefore see one prefix while new branches still used another. ## What changed - Await the existing settings mutation from the branch-prefix field. - Restore the authoritative saved prefix when that mutation rejects. - Preserve successful Enter and blur saves, validation, dirty drafts, the default `bb/`, and branch creation behavior. - Add a focused regression for the failed-save path. No daemon wire contract, server API, SDK API, CLI, documentation, or protocol version changed. ## How you verified - `pnpm exec turbo run test --filter=@bb/app -- --run src/views/SettingsView.branch-prefix.test.tsx` — 5 tests passed. - `pnpm exec turbo run typecheck --filter=@bb/app` — passed. - `pnpm exec oxfmt --check apps/app/src/views/SettingsView.tsx apps/app/src/views/SettingsView.branch-prefix.test.tsx` — passed. - `git diff --check origin/main...HEAD` — passed. - The original source thread also completed full app tests, app build, and matched desktop failure/success QA. > AGENT GENERATED
## Human comments ## What was wrong Linux AppImages were still built with electron-builder's legacy `appimage-12.0.1` toolset and its 2019 AppImage runtime (`effcebc`). Crash capture on the recurring CI signature showed that the zygote first received `SIGBUS/BUS_ADRERR` while fetching an executable instruction from the AppImage FUSE mount. The fault address equaled RIP at the first instruction of PDFium's `copy8`; the browser crash handler then terminated with `int3`/`SIGTRAP`, which is the outer signal reported by the lifecycle smoke. This is why AppImage stdout/stderr was normally empty: the failure was below app JavaScript and before owned-runtime publication. The captured system had 14 GiB available, untouched swap, and no OOM evidence. The same signature occurred on Intel and AMD CI runners. The core and matched Electron 41.7.0 symbols ruled out the Chromium sandbox CHECK addressed by get-bb#2657, the post-marker port race addressed by get-bb#2822, an application fatal path, and CPU-vendor-specific behavior. ## What changed - Upgrade electron-builder to 26.15.7 and explicitly pair its exact Squirrel Windows peer. - Select AppImage toolset 1.0.3, which packages the maintained 20251108 static runtime instead of the legacy external-libfuse runtime. - Extend the existing packaging-config regression test to require toolset 1.0.3. The resulting x86_64 AppImage is a static PIE and reports runtime commit `dd6cebe`. There is no server/host-daemon wire change, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. ## How you verified Red, before the fix: - [Diagnostic CI run 33574359370](https://github.com/get-bb/bb/actions/runs/33574359370) reproduced the exact pre-runtime `SIGTRAP` on iteration 3 after two healthy launches. - Its zygote core recorded `SIGBUS`, `si_code=BUS_ADRERR`, and `si_addr == RIP`; the matching Electron build ID resolved the address to the first instruction fetched from the FUSE-backed executable. - Memory, swap, kernel logs, browser and zygote cores, the exact AppImage, and matched symbols were inspected. All diagnostic load was sequential and bounded, and failure stopped the loop. Green, with the fix: - On the same bounded six-launch workflow and runner class, the candidate completed five target launches without a pre-runtime signal; iteration 6 then exposed the distinct post-marker owned-runtime flake and produced no crash core. - The exact same candidate SHA passed all six AppImage mount/lifecycle launches on [attempt 2 of run 33575866378](https://github.com/get-bb/bb/actions/runs/33575866378). - [PR CI run 33576754644](https://github.com/get-bb/bb/actions/runs/33576754644) passed the full matrix, including normal Linux AppImage packaging and lifecycle smoke. - `pnpm exec turbo run build typecheck test --filter=@bb/desktop --concurrency=2`: 15/15 tasks passed; 38/38 desktop test files passed, 247 tests passed, one skipped. - `pnpm install --frozen-lockfile` and `git diff --check` passed. > AGENT GENERATED: by GPT-5.6-Sol
## Human comments ## What was wrong After a text setting blurred and began a slow autosave, typing a newer draft before the request finished could be undone by the older response. The reconciliation effect replaced the newer unsaved text with the earlier stored value. ## What changed - Track whether a string setting has a draft newer than its last blur. - Reconcile stored values only when no newer local draft exists. - Keep the shared behavior for regular, multiline, and secret string settings without changing validation or autosave semantics. - Add a focused regression that completes the older request, preserves the newer draft, and then saves it normally. No daemon wire contract, server API, public Plugin SDK API, CLI, documentation, or protocol version changed. ## How you verified - `pnpm exec turbo run test --filter=@bb/app -- --run src/components/plugin/PluginSettings.test.tsx` — 12 tests passed. - `pnpm exec turbo run typecheck --filter=@bb/app` — passed. - `pnpm exec oxfmt --check apps/app/src/components/plugin/PluginSettings.tsx apps/app/src/components/plugin/PluginSettings.test.tsx` — passed. - `git diff --check origin/main...HEAD` — passed. - The original source thread also completed app build and matched slow-save browser QA. > AGENT GENERATED
## Human comments ## What was wrong The project selector menu had no viewport-aware height limit or vertical scroll behavior. A long project list extended beyond short screens. ## What changed The project selector menu now uses the available Radix height and the dynamic viewport height. It scrolls vertically and contains overscroll. ## How you verified - `pnpm exec turbo run test --filter=@bb/app -- --run src/components/pickers/ProjectSelector.test.tsx` - `pnpm exec turbo run typecheck --filter=@bb/app` - Browser test at 900 by 320 pixels with 20 projects. The 615-pixel list scrolled within a 190-pixel menu. > AGENT GENERATED
## Human comments ## What was wrong get-bb#2884 centralized atomic queue admission, whole-group eligibility, failed-row exclusion, and manual-Stop policy, but automatic dispatch orchestration was still split across lifecycle events, timer and plugin walkers, wait-release handlers, and recovery. Those callers independently chose candidate rows, pacing, coalescing, and failure behavior. In particular, the `turn-started` path captured queued row IDs and only logged terminal failures, while other drains persisted them, and plugin recheck coalescing used process-global state rather than server-instance state. The manual-Stop pause also classified ordinary queued rows as only unwaited or `thread-busy`. A user message parked while a turn was starting uses `turn-starting`, so an automatic drain could claim that older row after Stop and unexpectedly restart the thread. The initial coordinator migration also grouped idle, scheduled, and orphan recovery under one periodic job guard and expanded plugin removal into one deferred task per affected thread. A slow idle pass could therefore suppress due-schedule recovery on overlapping ticks, while removing a plugin could create an unbounded burst of deferred queue work. ## What changed - Replaced the old `queue-drains` module and automatic-send entry points with one queued-message dispatch coordinator whose interface accepts typed wake-up facts. - Migrated thread lifecycle, turn events, workspace readiness, provisioning settlement, interaction settlement, host reconnect, plugin recheck/removal, message creation, and periodic recovery to that interface. - Moved candidate discovery, wait release, per-thread dispatch coalescing, requeue pacing, automatic eligibility, claims, timeout treatment, persistent failure handling, and orphaned-plugin recovery behind the coordinator seam. - Kept lifecycle wake-ups immediate even after a recent requeue while timer and plugin sweeps retain their anti-spin cooldown. - Kept periodic idle, due-scheduled, and orphaned-plugin recovery as independently guarded typed wakes, so an overlapping tick can deliver due work while an earlier idle pass is still running. - Kept plugin-unregister release and dispatch as one sequential coordinator batch instead of one deferred task per affected thread. - Scoped plugin-recheck request coalescing to each server instance. - Kept explicit “Send now” separate behind an explicit-send-only interface. - Classified `turn-starting` user rows as ordinary for manual-Stop pause policy, while keeping explicit Send now as the deliberate override. There is no host-daemon wire, database schema, CLI, SDK, migration, or documentation contract change. ## How you verified - Added regressions for terminal failure persistence on `turn-started`, lifecycle wake-ups during the requeue cooldown, per-server request coalescing, and orphaned plugin waits. - Added a public HTTP/session-event regression covering steer during startup, manual Stop, a late `turn/started`, a later queued message, automatic `thread-ready` dispatch, and explicit Send now. It failed before the predicate fix and passed after it. - Added an overlapping-periodic-tick regression that blocks idle recovery and proves due scheduled work gets the next dispatch opportunity. It failed before independent guards and passed after. - Added a plugin-unregister regression covering multiple affected threads. It failed with two deferred tasks before the batch fix and passed with one afterward while draining both rows. - Final focused Turbo queue/lifecycle matrix: 69/69 server tests passed. - `@bb/server` Turbo typecheck and build passed. - Full server suite before the review follow-up: 2,134 passed. The ten unrelated `install-machine-script` cases failed both in the full run and alone because this host has `/tmp/package.json` with `"type": "module"`, which makes their extensionless CommonJS fixture fail with `ReferenceError: require is not defined`. - Changed-file formatting and `git diff --check` passed. Follow-up to get-bb#2884. > AGENT GENERATED
## Human comments ## What was wrong The dispatch checkpoint handled scheduled sends, busy threads, provisioning, and pending interactions, but it did not treat a disconnected host as a core wait. A follow-up to an existing idle thread therefore continued into live host dispatch and could fail with a 502 instead of becoming durable queued work. ## What changed The dispatch checkpoint now resolves the attached environment and its live host status before dispatching. When a usable thread environment belongs to a disconnected host, the message is persisted with the existing `host-offline` wait reason. The public-route regression test covers the normal `steer-if-active` follow-up path on an existing idle thread. It then reconnects that same host through the real daemon socket-open handler and the centralized queued-message dispatch path from get-bb#2886, verifies the wait is released and the row is consumed, and verifies a `turn.submit` carrying the original input is issued on the original thread. This changes no server/daemon wire contract, CLI command, or user-facing setting, so no protocol version or documentation update is needed. ## How you verified - `pnpm exec turbo run test --filter=@bb/server -- test/public/public-thread-offline-followup.test.ts test/threads/queue-drain-failure.test.ts test/threads/requested-queue-drain.test.ts test/threads/thread-send-dispatch.test.ts` - All 43 focused queue, reconnect, and dispatch tests passed. - `pnpm exec turbo run typecheck --filter=@bb/server` - Prettier and `git diff --check` passed. - Live dev CLI: created an existing idle thread, held its host offline, and ran default `bb thread tell`. The command returned `delivery: queued` with `waitingOn.kind: host-offline`; `bb thread queue list` showed the same row. After reconnect, the queue emptied and `bb thread output` returned the queued prompt response on the original thread. Fixes: N/A (no linked issue) > AGENT GENERATED
## Human comments ## What was wrong Sonner's built-in toast stylesheet targets every descendant with a `data-icon` attribute and adds asymmetric icon margins. bb's shared `Icon` component uses the same attribute inside custom toast content, so the close glyph sat 3.5px left of its hover background's center; the leading status glyph inherited the same displacement. ## What changed Neutralized Sonner's inherited margins on the custom toast status and close icons. Added focused regression coverage for both glyphs. This has no wire, protocol, CLI, guide, or documentation impact. ## How you verified - `pnpm exec turbo run test --filter=@bb/app --force -- src/components/ui/app-toast.test.tsx` (4 tests passed) - `pnpm exec turbo run typecheck --filter=@bb/app --force` - `pnpm exec oxfmt apps/app/src/components/ui/app-toast.tsx apps/app/src/components/ui/app-toast.test.tsx --check` - `git diff --check origin/main...HEAD` - Doobie hover-state capture confirmed that the close glyph and its hover background now share the same horizontal center. > AGENT GENERATED
## Human comments This adds a notification center where you can see the full contents of all the notifications you got in 1 place. Currently it's needed cuz the notifications are actually clamped so you can't even see the full contents of a single notification without copy-pasting. https://github.com/user-attachments/assets/34c32b6e-66ec-43ac-9e2a-6640d6244080 Notification center can be accessed via command palette or just pressing "show more" on one of the longer notifications. ## What was wrong Long toast text was unreadable and unrecoverable, for two independent reasons. First, sonner 1.7.4 forces a collapsed non-front toast to the front toast's height — `[data-expanded="false"][data-front="false"] { height: var(--front-toast-height) }` — and `apps/app/src/app.css:594` adds `overflow: hidden` on that same node so our custom card is clipped rather than overflowing. The moment a second, shorter toast arrived, a long one was cut off; hovering sets `data-expanded="true"` and restores it, which is why the text was present but unreachable. Second, nothing recorded toasts, so a message that auto-dismissed after its four-second duration was gone for good. Together these made a long plugin install failure impossible to read — the reported symptom was a `Could not resolve "@radix-ui/react-tabs"` esbuild error that could only be recovered by copy-pasting it elsewhere. ## What changed - `apps/app/src/lib/notifications/notification-store.ts` (new): in-memory store recording every toast for the session, capped at 200. Reusing a toast `id` updates the entry in place, so a loading toast replaced by its result stays one row; transient `loading` toasts are not recorded. - `apps/app/src/components/ui/app-toast.tsx`: descriptions clamp to four lines and measure real overflow (`scrollHeight > clientHeight`). When clipped, the toast grows a **Show more** action that dismisses it and opens the center scrolled to that entry. Clamping also keeps toasts short, so the stacking bug cannot reproduce. - `apps/app/src/components/notifications/NotificationCenter.tsx` (new): bottom-right tray built on shared-ui's `Popover`, which already falls back to `ResponsiveDrawerShell` on compact viewports per the AGENTS.md drawer rule. Untruncated text, per-row copy and dismiss, "Clear all". Opening it dismisses the toast stack so the two do not overlap. - `apps/app/src/components/sidebar/SidebarNotificationsButton.tsx` (new) plus `AppSidebar`: sidebar bell badging the count raised since the center was last opened. - `packages/domain/src/app-keybindings.ts`, `apps/server/src/services/system/app-keybindings.ts`, `apps/app/src/lib/app-command-metadata.ts`: new `notifications.open` command, surfaced in the quick palette as "Show all notifications". It ships **unbound** rather than claiming a chord; Settings → Keyboard can assign one. - `apps/app/src/lib/notifications/plugin-toast-recording.ts` (new) and `plugin-frontend.ts`: plugins receive the raw `sonner` namespace from `installPluginRuntime`, bypassing `appToast`, so its `toast` export is wrapped to record plugin toasts too. - `apps/app/src/components/sidebar/PluginThreadList.tsx`: the only place in the app calling `sonner` directly; moved to `appToast`. - `packages/shared-ui`: new `Bell` icon (`Notification03Icon`). - `docs/configuration.md`: new Notifications section. `apps/app/src/components/ui/sonner.stories.tsx`: long-build-error case so the clamp is reviewable in the Ladle catalog. No wire changes: this is app-layer state plus one `AppCommandId` enum addition, and nothing crosses the server/daemon boundary, so `HOST_DAEMON_PROTOCOL_VERSION` is untouched. ## How you verified 26 new tests across the store, the plugin-toast wrapper, the toast clamp, and the tray. The clamp and Show-more tests fail before this change because neither the clamp class nor the action exists. - `pnpm exec turbo run test --filter=@bb/app` — 448 files, 3532 passed, 3 skipped - `pnpm exec turbo run test --filter=@bb/server` — 2072 passed - `pnpm exec turbo run test --filter=@bb/domain --filter=@bb/shared-ui` — passing - `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/domain --filter=@bb/server --filter=@bb/shared-ui` — clean Not yet verified visually: the clamp's visual balance and the popover's placement against the toast stack have not been eyeballed in a running app or in iOS Simulator Safari. Worth a look before merge. > AGENT GENERATED Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge upstream/main (75 commits through 88ee0d0) into the fork. Wire: HOST_DAEMON_PROTOCOL_VERSION 177 (upstream 176); the fork's admission RPCs change the wire so enrolled daemons must update. DB: the fork's admission/lease schema is regenerated as 0113_workspace_safety after upstream's 0110-0112 (drizzle-kit generate, IF NOT EXISTS on CREATE statements so databases that ran the old fork 0110 replay cleanly). Drizzle only applies journal entries newer than the latest applied one, so leaving the fork migration at 0110 would have silently skipped it on any database already at upstream's tail. Dispatch rework (get-bb#2779): the admission release on turn completion moves from the removed queued-message-auto-send follow-up to the queued-message-dispatch follow-up's thread-ready wake; daemon connect keeps the fork's admission reconcile and adds upstream's host-connected dispatch wake. Thread creation, the conversation-outline route and the thread count/running routes take upstream's implementation. The dead deferred_thread_messages exports and the removed provider-session-reaping flag are dropped. Fork-required fields (admission, protectUnmanagedWorkspace) are added to upstream's new test fixtures; the GitHub plugin's extraRepos schema uses the fork's repo parser; plugin SDK follows upstream 0.4.35 and the SDK inventory hash is regenerated.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eba92ab504
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| requestQueuedMessageDispatch(deps, { | ||
| hostId: args.hostId, | ||
| kind: "host-connected", | ||
| }); |
There was a problem hiding this comment.
Serialize dispatch behind admission reconciliation
When a socket drops after a turn becomes idle but before its admission release completes, the database can retain a running admission for that idle thread. Starting the host-connected drain concurrently with reconciliation lets a queued send reuse that stale reservation while reconciliation releases it; depending on ordering, the daemon then rejects the command or allocates a replacement reservation that is not persisted, leaking a host slot. Chain this wake after reconciliation and durable-admission recovery finish.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed, and fixed in #7. onDaemonSocketOpen now requests the host-connected wake in a .finally after reconcileHostWorkAdmissions and recoverDurableWorkAdmissions settle, so a drained send can no longer race a stale running admission that reconciliation is about to release. On a failed reconcile the wake still fires after the logged warning rather than stranding host-offline rows until the next reconnect; every dispatch still passes the daemon's fail-closed reserve, so that path degrades to a rejected command, not a leaked slot.
Covered by a new test that defers the reconcile reply and asserts no turn.submit dispatches until it is answered (public-thread-offline-followup.test.ts). No wire change, so HOST_DAEMON_PROTOCOL_VERSION stays at 177.
There was a problem hiding this comment.
One adjustment after CI on #7: the wake now runs only when reconciliation succeeds, gated on the opening session still being the host's registered daemon session, instead of in a .finally. The .finally form fired the wake after a dropped daemon (and, in tests, after the harness had closed its database whenever a fake daemon never answered reconcile: six unhandled "database connection is not open" errors on the server job). A failed reconcile means the daemon is unhealthy or gone; its next reconnect reconciles and wakes the parked rows. Covered by a second test that answers reconcile with an error and asserts the host-offline row stays parked.
| // A dispatch that arrived while this machine was away parked its row on a | ||
| // `host-offline` wait with no schedule, so no sweep can see it — the | ||
| // machine coming back is that wait's release signal, and this socket | ||
| // opening is where core hears it. |
There was a problem hiding this comment.
Remove the forbidden reconnect comment
This newly added prose comment is neither a semantic tool directive nor a Plugin SDK declaration comment, so it violates the repository's explicit prohibition on code comments and should be removed while keeping the behavior self-explanatory through naming.
AGENTS.md reference: AGENTS.md:L7-L9
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Removed in #7. The comment came in verbatim from upstream's side of the merge conflict; the wake's kind: "host-connected" carries the meaning on its own. For the record, the bb/no-comments rule only runs in the two packages that define a lint script (apps/app, apps/mobile), which is why this passed CI; the AGENTS.md policy still applies, so it is gone.
The Linux AppImage lifecycle smoke fails on the fork's GitHub-hosted runners with "The owned runtime exited before bb became ready" while passing upstream on Blacksmith. The runtime's stdout/stderr live only in the GUI's in-memory child log, so the failure prints updater noise and nothing else. Dump the tails of the bb-app launcher's log files under BB_DATA_DIR/logs into the error so the run carries the runtime's own exit reason.
…eady When the Electron-owned bb-app process exits before the server answers, its stdout/stderr only reach the in-window error page. Also write the exit result and the captured child log through the desktop logger so a headless launch (the Linux AppImage lifecycle smoke on CI) carries the runtime's own failure reason on stderr.
The AppImage lifecycle smoke fails on GitHub-hosted ubuntu-latest with "The owned runtime exited before bb became ready". The captured child log shows the nested runtime dying with exit 9 and "bad option: --no-sandbox": Ubuntu 24.04 restricts unprivileged user namespaces, the AppImage toolset's AppRun reacts by adding --no-sandbox to the payload args, and the desktop's nested runtime relaunch runs the same AppImage as Node, which rejects the flag. Upstream's Blacksmith runners do not restrict user namespaces, so lift the restriction on the runner before the smoke to match them, printing the prior value for the record.
…iliation On daemon connect the fork's admission reconciliation ran concurrently with upstream's host-connected dispatch wake. A socket that dropped after a turn went idle but before its admission release could leave a running admission row behind; a queued send drained on reconnect could reuse that stale reservation while reconciliation released it, so the daemon rejected the command or allocated a replacement reservation the server never persisted, leaking a host slot. Request the wake in a finally after reconcile and durable recovery settle; a failed reconcile still wakes host-offline rows after the logged warning, and every dispatch still passes the daemon's fail-closed reserve. Also drop the two prose comments Codex flagged on #5 and #6 and move the protocol-offset rationale into AGENTS.md. No wire change.
Human comments
What was wrong
The fork was 75 commits behind get-bb/bb after PR #5, missing the 0.41.0 release the user's desktop app already runs, the dispatch-queue rework (get-bb#2779: every send is a dispatch attempt, typed
waitingOnqueue rows, themessage.dispatchplugin checkpoint and theconcurrency-limitplugin), persisted conversation outlines (get-bb#2668), the notification center, and the steer-on-Enter default. Two of those upstream changes collided with fork-owned code: the rework replaced thequeued-message-auto-sendfollow-up the fork used to release a thread's work admission on turn completion, and upstream shipped migrations0110–0112, so the fork's admission/lease migration, also numbered0110, would have been silently skipped by Drizzle on any database that had already applied upstream's tail (the user's live~/.bb/bb.dbis one), leaving the admission tables missing at runtime.What changed
integration/upstream-2026-08-31) so the diff shows only this batch; retarget tomainonce Integrate upstream post-0.40 stability batch and fix fork CI reds #5 lands.88ee0d012) preserving the fork features: workspace admission/leases andprotectUnmanagedWorkspace, provider-process ownership and diagnostics, the DB read worker, timeline fork history, explicit GitHub repo tracking, GitHub-hosted CI runners.HOST_DAEMON_PROTOCOL_VERSIONbumped to 177, ahead of upstream's 176, because the fork's admission RPCs change the wire; enrolled fork daemons must update.drizzle-kit generateas0113_workspace_safetyafter upstream's0112, withIF NOT EXISTSon its CREATE statements so databases that already ran the old0110replay cleanly;migrate.ts's protect-column staging hack and the migrate tests point at the new tag. Upstream's0110–0112and their snapshots are taken verbatim.queued-message-dispatchfollow-up (thread-readywake) inapps/server/src/internal/events.ts; daemon connect runs the fork's admission reconcile and upstream'shost-connecteddispatch wake.count/runningroutes take upstream's implementation; the fork's read worker keeps serving thread-list and timeline snapshots.deferred_thread_messagesdata exports and the removed provider-session-reaping experiment flag (upstream deleted both).admission,protectUnmanagedWorkspace) added to upstream's new test fixtures in the app, CLI, andconcurrency-limitplugin; the GitHub plugin's newextraReposschema validation uses the fork's repo parser.ubuntu-latestwith "The owned runtime exited before bb became ready", while upstream is green on Blacksmith. Two diagnostics landed to find it and stay: the smoke script now dumpsBB_DATA_DIR/logstails on failure, and the desktop logs the captured child output when bb-app exits before the server is ready. That log showed the nested runtime dying with exit 9,bad option: --no-sandbox: Ubuntu 24.04 restricts unprivileged user namespaces, the AppImage toolset's AppRun (from upstream Fix AppImage pre-runtime SIGTRAP flake get-bb/bb#2885's electron-builder upgrade) responds by adding--no-sandboxto the payload args, and the desktop's nested runtime relaunch runs that same AppImage as Node, which rejects the flag. CI now liftskernel.apparmor_restrict_unprivileged_usernsbefore the smoke, matching upstream's runner environment. Real Ubuntu 24.04 users of an AppImage build would hit the same failure; that is an upstream bug worth reporting separately.How you verified
Run under
fnm exec --using 22.22.1unless noted, one turbo run at a time.pnpm exec turbo run typecheck: green across the repo (initial run surfaced 13 errors from the merge, all fixed).pnpm exec turbo run lint: green.pnpm exec turbo run testfor@bb/db(465 tests),@bb/host-daemon-contract,@bb/agent-runtime,@bb/host-daemon,@bb/cli,@bb/demo-server,@bb/plugin-api-map,bb-plugin-github,bb-plugin-concurrency-limit: green. The migrate tests that pin the fork migration's timestamp failed before the re-tail and pass after.@bb/server(Node 22.23.2): full suite 2188/2196, then the 8 remaining failures fixed and their three files re-run green: two fork read-worker tests and the fork-adapted outline-reuse test asserted the outline route runs in the worker, which now takes upstream's persisted-outline path (tests follow upstream); three of upstream's newturn-starting queue waittests asserted aturn.submitcommand synchronously, but the fork's admission reserve is an async host RPC, so they now wait withvi.waitForlike the fork's other dispatch tests; the other two were 5s load timeouts that pass alone.@bb/app: 3724/3733 in the full run, the 5 timeouts (4 files) pass when re-run alone.@bb/integration-tests(Node 22.23.2): 77/77. Note: on Node 22.19–22.22 the fork's tsx read worker cannot map a relative.jsimport to.tsinside a worker thread and every harness times out; Node 22.23.2 (CI's22.x) and 24 resolve it. This is pre-existing and local-only (PR Integrate upstream post-0.40 stability batch and fix fork CI reds #5 noted the same symptom); it is not touched by this merge.git log <merge-base>..upstream/main -- <file>provenance for each of the 22 conflicted files.