Landing page elevation: real bb surfaces, section rhythm, and a rebuilt demo set - #6
Landing page elevation: real bb surfaces, section rhythm, and a rebuilt demo set#6lnittman wants to merge 308 commits into
Conversation
## What was wrong The first-run setup guide shipped behind the `newOnboarding` experiment, which defaults to false (`packages/domain/src/experiments.ts`) and was never enabled by default. Nobody saw the flow unless they turned the toggle on by hand, so its UI, its telemetry funnel, its persisted completion timestamp, and a dedicated host-daemon command existed to serve a surface that never ran — spread across seven packages. ## What changed Deleted the experiment and everything that existed only for it: - `OnboardingHost` / `OnboardingFlow` (828 lines) and their `App.tsx` mount. - The `newOnboarding` experiment key, its Settings → Experiments toggle (web and `apps/mobile`), and every config fixture that listed it. - `appSettingsSchema.onboardingCompletedAt`, the Settings → General "Setup guide" replay control, and `bb settings replay-onboarding`. No migration is needed: since get-bb#102 app settings are key/value rows read through `inArray(key, appSettingsKeys)`, so the retired key's row is ignored. The legacy wide `app_settings` table keeps its column, which is deliberately frozen for `seedKeepAwakePluginConfiguration`. - The five onboarding funnel telemetry events, `POST /system/onboarding/event`, and `sdk.system.onboardingEvent`. - `GET /system/onboarding/repos`, `sdk.system.onboardingRepos`, and — since nothing else sent it — the `workspace.discover_repos` daemon command with its 460-line handler. **Wire change: `HOST_DAEMON_PROTOCOL_VERSION` 137 → 138**, because a command left the protocol. The reason is recorded in the `protocol.ts` header. Docs and agent surfaces updated in the same change: `bb-guide-customization.md` (regenerated), the `bb-cli` SKILL and its `app-settings` reference, `bb-plugin-authoring`'s SDK method table, and `docs/configuration.md`. `getOnboardingAgentOverview` and `GET /system/onboarding/agents` deliberately stay. Despite the name they are not onboarding-only: the root composer resolves an unset provider selection through `useOnboardingAgents` (`useThreadCreationOptions.ts` ← `RootComposeView.tsx`), which ships unconditionally. Renaming that endpoint and its SDK method to match its real purpose is a follow-up, not part of a deletion. `useOnboardingAgents` does lose its `poll` option, which became constant once the flow's caller went away. Two test adjustments worth calling out, both because their subject was deleted rather than because they broke: - `apps/cli` had a test using `onboardingCompletedAt` as its nullable-string case for `bb settings general`. `appSettingsSchema` now holds only booleans, so that path has no live key to exercise; the test is trimmed to the unknown-key assertion it also covered. The generic value-parsing code in `updateGeneralSetting` is untouched and still accepts `null`. - `packages/db`'s key/value migration test no longer asserts a settings field that no longer exists. ## How you verified Nothing here is a behavior fix, so the evidence is that removing a never-enabled surface changes nothing else: - `pnpm exec turbo run typecheck` — 72/72 tasks green. - `pnpm exec turbo run test` for `@bb/app` (3037), `@bb/server` (1787), `@bb/mobile` (812), `@bb/host-daemon` (47 files), plus `@bb/db`, `@bb/domain`, `@bb/sdk`, `@bb/cli`, `@bb/desktop`, `@bb/server-contract`, `@bb/host-daemon-contract`, `@bb/templates`, `@get-bb/plugin-sdk` — all green. - `pnpm exec turbo run lint` — 0 errors (144 pre-existing react-compiler warnings in `@bb/app`, unchanged). - `packages/host-daemon-contract`'s protocol-version test pins 138, and its command-fixture map no longer accepts `workspace.discover_repos`, so a stray sender fails the contract test. - Repo-wide grep for `newOnboarding`, `onboardingCompletedAt`, `replay-onboarding`, `onboardingRepos`, `onboardingEvent`, and `discover_repos` returns only the `protocol.ts` changelog note. No CHANGELOG entry: per `docs/bb-release-process.md` those sections are written at release-prep time, and this removes nothing a user could see. Fixes: N/A — no linked issue. > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…get-bb#2009) ## What was wrong The mobile app stacked two headers on the thread screen (the native bar plus a second title / status / environment / actions block), new threads went through a separate `/compose` page, and the composer always showed its full pill rows and a context readout. Picker sheets clipped their last row under the home indicator because the `scroll` layout rendered the title outside the measured scroll view, so dynamic sizing came up short by the header height. ## What changed - **Thread screen**: one native header — title (tap → rename) with a status subtitle only while working / needs input / error, panel + `…` on the right. Environment line, child roll-up, Workspace and the git action live in the `…` sheet (`leadingActions` / `headerDetail`). Table of contents removed end to end. - **Composer**: `collapsible` / `topControls` / `onExpandedChange`. Collapsed = `[+] placeholder [mic | Stop while running]`; expanded ⇔ focused (or has content). Sheets opened from inside report presence through `SheetPresenceContext` so the card stays open and refocuses after a picker. Context-window readout is a ring shown only at ≥60%. `PickerTrigger` is ghost by default. Pill text nudged up 3pt on iOS to centre on the buttons. - **Home**: `ComposeDock` replaces the FAB and the `/compose` route. It expands in place over a scrim; the drawer header is painted the same blend (`blendOver`) so the whole screen dims. `composeHref` → `newThreadHref` (home with params + `newThread=1`); drawer row, project "+", fork, handoff, new project, share intent and `bb://compose` all route there; home reads/clears the params. Controller re-seeds per param change and drops the dead `title` knob. Thread rows drop their timestamps (`relative-time` removed). - **Sheets**: the `scroll` layout keeps the title as a sticky first child inside `BottomSheetScrollView` so dynamic sizing includes it; the provider CLI log sheet pads `insets.bottom`. - Also removes the stale `claudeCodeMockCliTraffic` experiment row (pre-existing typecheck break). ## How you verified - `pnpm exec turbo run typecheck lint test --filter=@bb/mobile` green (812 tests; new tests for `blendOver`, `/compose` link mapping). - Maestro against the harness backend on a Debug dev client: `phase1-shell`, `phase3-compose`, `phase4a-timeline`, `phase4b-send`, `phase4b-actions`, `phase4b-thread-actions`, `phase4b-queue`, `phase4b-approve`, `phase4b-ask-user`, `phase6-panel` pass (flows updated for the moved controls and the pill-until-focused composer). - Visual checks in the iPhone 17 Pro simulator, light and dark: home pill → card, picker keeps it open, scrim + header dimming, create → thread, fork → dock with hint, thread header + menu, picker sheets with full content and bottom inset. Fixes #N/A > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong The [failing main CI run](https://github.com/get-bb/bb/actions/runs/32336675998) passed all 1,089 app-shard assertions, then reported an unhandled `scrollContainer.removeEventListener is not a function` exception from `PromptBoxInternal.test.tsx`. TipTap React intentionally destroys editor instances on a 1 ms timer after unmount so Strict Mode can reuse them. The suite returned from `afterEach` immediately after React cleanup, allowing the final delayed editor destruction to race Vitest jsdom environment shutdown on a loaded CI worker. ## What changed Wait for TipTap deferred editor destruction after cleaning up each `PromptBoxInternal` test. This keeps editor and scroll-listener teardown inside the jsdom environment that created them and prevents state from leaking across tests or into environment shutdown. There are no product, wire-protocol, CLI, guide, or documentation changes. ## How you verified - Fail-before evidence: main CI app shard completed 132 files and 1,089 assertions, then failed during delayed TipTap destruction. - `pnpm exec turbo run test --filter=@bb/app --force -- src/components/promptbox/PromptBoxInternal.test.tsx` — 99 tests passed. - `pnpm exec turbo run test --filter=@bb/app --force -- --shard=3/3` under concurrent typecheck load — 132 files and 1,089 tests passed. - `pnpm exec turbo run typecheck --filter=@bb/app` — passed. - `git diff --check` — passed. Fixes: N/A — CI teardown flake. > AGENT GENERATED: by GPT-5
## What was wrong When Claude Code resumed a session that had orphaned background work, the SDK could drain a zero-work `result` with `origin.kind=task-notification` immediately before processing the newly queued human prompt. The Claude bridge emitted `input.accepted` as soon as it queued that prompt, and the translator allowed any result to claim a pending accepted input. The recovered notification therefore manufactured and completed an empty turn for the new message; the real answer continued later under a second, unaccepted turn. This is the mechanism independently reproduced in [get-bb#1718](get-bb#1718) and observed again in [the affected production thread](https://ymichael.getbb.app/projects/proj_qrv2s45kmq/threads/thr_gcuc46ug4j). ## What changed - Parse Claude result provenance and allow an idle result to claim pending input only when its origin is human (or omitted, which the SDK defines as human). A non-human result can still settle work when a turn is already open, preserving live background-notification loops and human zero-work commands such as `/clear`. - Make Claude `turn/start` match `turn/steer`: emit acceptance and answer the command only after the SDK prompt iterator consumes the queued input. - Add regressions for the recovered-task-notification sequence and for turn/start consumption ordering. Existing bridge tests now follow the same consumed-before-accepted contract. - Bump `HOST_DAEMON_PROTOCOL_VERSION` from 138 to 139 because older daemons emit the incorrect lifecycle semantics. The uniform provider rule is: `input.accepted` means the provider consumed the input, never merely that bb queued it. Codex already follows that rule. Pi's prompt promise currently reports settlement rather than consumption, and ACP exposes no equivalent provider acknowledgement, so those bridges need provider-specific correlation work rather than a timing heuristic in this focused Claude fix. The Pi/ACP cross-provider follow-up is tracked in [get-bb#2014](get-bb#2014). There are no CLI, guide, configuration, or user-facing documentation changes. ## How you verified - Before the implementation, the new provenance regression received `turn/started` + `turn/input/accepted` + `turn/completed` instead of no events, and the new turn/start ordering regression observed a response before SDK consumption. - `pnpm exec turbo run test --filter=bb-plugin-provider-claude-code --force` — 259 tests passed, including `/clear` zero-work conformance. - `pnpm exec turbo run typecheck --filter=bb-plugin-provider-claude-code --force` — passed. - `pnpm exec turbo run test typecheck --filter=@bb/host-daemon-contract --force` — 52 tests passed; typecheck passed. - `pnpm exec turbo run typecheck --filter=@bb/host-daemon --force` — passed. - `pnpm exec turbo run typecheck --filter=@bb/server --force` — passed. - `git diff --check` — passed. Fixes get-bb#1718 > AGENT GENERATED: by GPT-5
## What was wrong Sent-message editing had three related compatibility assumptions: - After the narrow provider-bridge grammar moved canonical timeline assembly into the runtime, bb turn IDs and native Codex turn IDs became intentionally different, but the server still sent the bb timeline ID as the Codex rewind checkpoint. - The Codex bridge only persisted checkpoints for successfully completed turns, even though Codex also persists and accepts interrupted turn IDs as fork boundaries. Editing after a stopped turn therefore fell back to a bb ID such as `da2f291120-t3`, which Codex correctly rejected as absent from its source thread. - The web client called `crypto.randomUUID` directly even though some supported browser contexts expose Web Crypto `getRandomValues` without `randomUUID`, causing the edit action to throw before the editor opened. The failures were reproduced from the originally reported thread and from the interrupted-turn repro in `thr_kfzu8tqu2d`. ## What changed Codex rewinds now use the persisted native `providerCheckpointId`. The compatibility fallback is restricted to UUID-shaped turn IDs from legacy Codex timelines, so a runtime-minted bb ID can never be forwarded to Codex. The Codex bridge now persists the native checkpoint for completed and interrupted `turn/completed` statuses. This makes edits after a stopped Codex turn use the fork point Codex actually emitted. Failed turns remain unstamped because older Codex rollouts may omit them and no equivalent fork proof exists for that status. Web edit sessions now create operation IDs through `nanoid`, which is already an app dependency and works when `crypto.randomUUID` is unavailable. ID generation remains inside the edit click handler. These changes populate and validate the existing `providerCheckpointId` / `retainThroughProviderCheckpoint` fields; they do not change the server/daemon wire contract, so `HOST_DAEMON_PROTOCOL_VERSION` is unchanged. There are no CLI, guide, or configuration changes. ## How you verified - Direct `codex app-server` `thread/read` showed the interrupted native turn ID, and an ephemeral `thread/fork` using it as `lastTurnId` succeeded. - Before the original server implementation change, the focused suite failed 8 Codex cases with `checkpoint-first` expected and `turn-first` received. - `pnpm exec turbo run test --filter=@bb/server -- --run test/threads/thread-edit-message.test.ts` — 42 passed. - `pnpm exec turbo run test --filter=bb-plugin-provider-codex -- --run src/delta-translation.test.ts src/bridge/bridge.zero-work-turn.test.ts` — 61 passed, including the full `thread/stop` → interrupted completion checkpoint path. - `pnpm exec turbo run typecheck --filter=@bb/server` — passed. - `pnpm exec turbo run typecheck --filter=bb-plugin-provider-codex` — passed. - Before the web implementation change, the compatibility test failed with `TypeError: crypto.randomUUID is not a function`. - `pnpm exec turbo run test --filter=@bb/app --force -- --run src/views/thread-detail/sent-message-edit-operation-id.test.ts` — passed. - `pnpm exec turbo run typecheck --filter=@bb/app` — passed. - `pnpm exec turbo run lint --filter=@bb/app` — passed with 0 errors and 144 existing warnings. Fixes the reported sent-message edit failures. > AGENT GENERATED: by GPT-5
## What was wrong React Strict Mode in the development app replays Pierre's ref callback against the retained diff custom element. The replacement renderer hydrates the existing plain `<pre>`, records it as already highlighted, and starts a worker task. Its normal render then early-returns because the file is unchanged. When the worker returns the highlighted AST, Pierre suppresses the repaint because the hydrated cache says it is already highlighted. Production does not perform this simulated remount, which is why `pnpm start` worked while `pnpm dev` did not. The packages CI job also exposed a separate Connect test race: the connected status renders before the asynchronous mobile-pairing capability response, but three tests synchronously queried the capability-gated button. ## What changed Added a development-only host adapter around Pierre's public `onPostRender` and `rerender()` APIs. A mount schedules one microtask after React's ref replay: the discarded renderer has already been disabled and safely no-ops, while the retained renderer takes Pierre's normal forced-render path so its worker completion can repaint. The adapter preserves plugin callbacks and stable option identities, covers the plugin `File`, `FileDiff`, `MultiFileDiff`, `PatchDiff`, and `UnresolvedFile` surfaces, and also covers the built-in diff card. Production receives the original options object unchanged. Removed the `pnpm` patched dependency and its renderer-internals regression; the install now uses stock `@pierre/diffs` 1.2.9. Added a host-level regression for the Strict Mode replay ordering. The Connect tests now await the mobile-pairing button before interacting with it. This is frontend/test-only; there are no host daemon protocol, CLI, or documentation changes. ## How you verified - The new recovery regression models the discarded and retained renderers and verifies that repaint waits until after ref replay. - `pnpm install --frozen-lockfile --ignore-scripts` passed with unmodified `@pierre/diffs` 1.2.9. - `pnpm exec turbo run test --filter=@bb/app --force`: 399 files / 3,041 tests passed (3 skipped). - `pnpm exec turbo run test --filter=@bb/app --filter=bb-plugin-github --filter=bb-plugin-connect --force`: app, GitHub (21 tests), and Connect (90 tests) passed. - `pnpm exec turbo run typecheck --filter=@bb/app --filter=bb-plugin-github --filter=bb-plugin-connect --force`: passed. - `pnpm exec turbo run lint --filter=@bb/app --force`: passed with 0 errors (pre-existing warnings only). - `pnpm exec turbo run build --filter=@bb/app --force`: passed. - Sawyer Hood's dev-browser against a real GitHub pull request under `pnpm dev` found 39 syntax-token spans with 6 distinct token styles in the original reproduction file. - Reproduced the Connect failure locally before awaiting the capability-gated button; its complete 90-test suite passes afterward. - `git diff --check`: passed. Fixes: development-only GitHub plugin diff syntax highlighting > AGENT GENERATED: by GPT-5
…op button (get-bb#2015) ## What was wrong Four mobile polish issues from dogfooding on a phone: - Every dimming overlay (home compose scrim, sheet backdrops) used `tokens.ink` at 35%. Dark palettes have a light `ink`, so the overlay lightened the screen to gray instead of dimming it. - `KeyboardPaddingView` subtracted the full home-indicator inset when the keyboard opened, so composers sat flush against the keyboard. - In the flat thread list the project name under a title was plain text and read like a second title. - The collapsed composer's Stop control was a square `secondary` button with a stroked square icon, flush with the pill edge. ## What changed - `apps/mobile/src/theme/scrim.ts`: `scrimBaseColor(mode, tokens)` — `ink` in light mode, black in dark mode. Used by the home compose scrim + dimmed header (`HomeScreen.tsx`), the bottom-sheet backdrop (`ui/Sheet.tsx`), and the navigation drawer overlay (`app/(drawer)/_layout.tsx`). - `ui/KeyboardPaddingView.tsx`: new `keyboardGap` prop and `COMPOSER_KEYBOARD_GAP = 8`; applied to the home dock, thread composer, and composer showcase. - `screens/sidebar/SidebarRows.tsx`: thread-row subtitle is now `{kind:"project"} | {kind:"snippet"}`; project subtitles render a `Folder` icon. Search snippets stay plain. Archived/search screens updated. - `composer/Composer.tsx`: `StopButton` — a 36pt round `secondary` circle with a filled square, used in both the collapsed pill and the expanded footer. ## How you verified - `pnpm exec turbo run typecheck lint --filter=@bb/mobile` pass. - New `scrim.test.ts` asserts the scrim darkens `background` for every palette × mode (fails with the old `ink` scrim in dark mode). - Simulator (iPhone 17 Pro, dark mode) against the local dev server: home compose scrim, display-options sheet backdrop, keyboard gap, project folder subtitle, and collapsed Stop button during a live turn. - Release build installed on a physical iPhone for the scrim/gap/folder changes. > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong A remote bb page received only the primary server host's editor-helper port, then probed that port on the browser's own loopback interface. On an enrolled secondary machine, the matching local daemon commonly listens on another port (for example `38888`), so Settings → Files could not reach it. The helper also required a duplicate remote-origin setting, and SSH target configuration could not select a host when the server had multiple machines. ## What changed - Report each daemon's full local API port during session open and expose the deduplicated connected-port candidates in system config. - Probe advertised candidates in parallel only after local-network access is available, selecting the helper whose reported server origin matches the current page. If none responds, retry the same candidates twice at one-second intervals. - Automatically allow the exact enrolled server origin in the local helper's CORS policy. - Add `--host-id` support to `bb-app client ssh-target set/remove`, retaining single-host auto-selection and server-wide removal behavior when omitted. - Bump `HOST_DAEMON_PROTOCOL_VERSION` from 138 to 139 so enrolled daemons update for the wire change. - Update CLI help, the builtin bb CLI skill, configuration docs, and the multiple-devices guide. ## How you verified Added coverage that fails without the new behavior for daemon port reporting, server candidate aggregation, exact-origin CORS access, browser origin selection, the two one-second retries, and host-specific SSH target configuration. - `pnpm exec turbo run typecheck --filter=@bb/app --filter=@bb/server --filter=@bb/host-daemon --filter=bb-app` - `pnpm exec turbo run test --filter=@bb/app -- --run src/lib/local-host-daemon-access.test.ts src/lib/system-config-atoms.local-access.test.ts` - `pnpm exec turbo run test --filter=@bb/server -- --run test/system/local-helper-ports.test.ts test/app/hub.test.ts` - `pnpm exec turbo run test --filter=@bb/host-daemon -- --run src/app.test.ts src/local-api.test.ts src/server-client.test.ts src/server-connection.test.ts` - `pnpm exec turbo run test --filter=@bb/host-daemon-contract -- --run test/contract.test.ts` - `pnpm exec turbo run test --filter=bb-app -- --run test/index.test.ts` - After rebasing onto `main`: `pnpm exec turbo run test --filter=@bb/app -- --run src/lib/local-host-daemon-access.test.ts src/lib/system-config-atoms.local-access.test.ts src/components/settings/MachinesSettingsSection.test.tsx src/views/MachineSettingsView.test.tsx src/components/settings/UpdatesSettingsSection.test.tsx` (63 tests passed) - Prettier checks and `git diff --check` Fixes: N/A (no linked issue) > AGENT GENERATED: by GPT-5
…et-bb#1876) ## What was wrong BB rendered code in two independent places and had no way for a plugin to change either. The file preview drove `@pierre/diffs`' `File` view and the diff card drove its `FileDiff` view, each assembling its own options record, its own line-selection wiring, and its own code-theme lookup. A plugin that wanted to render source or a diff had a third path: import `@pierre/diffs` directly through the runtime shim and rebuild the host's behavior by hand. `plugins/github` did exactly that — synthesizing a `diff --git` header for GitHub's REST patches, calling `parsePatchFiles`, and running two MutationObservers (one on the root `class`, one on `data-bb-code-theme-*`) to keep Pierre's theme in step with BB's. That is three copies of one capability, and it made "change how bb renders code" impossible to express: there was nothing to replace. ## What changed **One host boundary per capability.** `SourceCodeHost` and `DiffHost` (`apps/app/src/components/code/`) resolve an exclusive plugin replacement through the same `resolveReplacement` + `PluginReplacementSlot` path the sidebar thread list and file openers already use, and otherwise render BB's own renderer. Both BB renderers (`BbSourceCode`, `BbDiff`) sit behind `lazy()`, so a replacement that never delegates never downloads them and `experimental_Original` costs nothing until it is rendered. **Public API** (all `experimental_`, with entries in `docs/api_to_audit.md`): ```ts experimental_SourceCode: { content, path, overflow?, highlightedLines?, className? } experimental_Diff: { patch, path, view?, overflow?, showLineNumbers?, className? } app.slots.experimental_sourceCodeRenderer({ id, title, description?, component }) app.slots.experimental_diffRenderer({ id, title, description?, component }) ``` A replacement receives fully resolved semantic props plus a bound `experimental_Original`, so it can delegate per call without re-entering resolution. Host-only inputs — the pre-parsed `ParsedGitDiffFile`, the raw patch text, the highlighter cache key, selection-to-composer — never cross the boundary, and no `@pierre/diffs`, Shiki, `FileOptions`, or `ParsedGitDiffFile` type appears in the public contract. `experimental_Diff` owns patch normalization, so a patch with no `diff --git` header (GitHub REST, a bare `@@` hunk) renders without the caller synthesizing one; content that parses to no hunks degrades to plain monospace text instead of an empty diff. **Migrations.** The native file preview, timeline file diffs, and the environment diff panel's file bodies all render through the boundary, so one registration covers BB's surfaces and plugin surfaces alike. `plugins/github` renders `experimental_Diff` and drops its `@pierre/diffs` devDependency along with both MutationObservers; its bundle no longer references the Pierre runtime shim (417.0 KB → 409.0 KB). The shim itself stays for compatibility with existing plugins. **User control.** Settings → Appearance gains **Source code** and **Diffs** rows beside **Sidebar** — Automatic / bb (built-in) / each registered provider, per client, each row hidden when no plugin supplies that renderer. A renderer takes over surfaces there is otherwise no route back from, so this is what keeps "installing activates it" reversible. `lib/plugin-replacement-preference.ts` now owns that automatic/built-in/named-provider rule and the thread list consumes it too, retiring its private copy and the `resolveThreadListReplacement` wrapper only its own test still called. Registered renderers also appear in the plugin detail's capability list. **Two cleanups fell out.** The opaque `diffViewOptions: Record<string, string | boolean | number>` threaded through five components became the semantic `DiffPresentation` (`view` / `overflow` / `showLineNumbers`). The renderers own light/dark selection themselves, which made the `themeType` prop chain from `ThreadTimelineSurface` down to `TimelineFileDiffBlock` dead; it and `ThreadTimelineTheme` are removed. **Docs and generated artifacts.** `docs/api_to_audit.md` gains three entries (components, slots, `experimental_Original`); the `bb-plugin-authoring` skill and the `bb guide` plugins chapter now point at the host components rather than a direct `@pierre/diffs` import, and the skill's own coverage test is extended so the new slots and their props stay documented. Bundled SDK declarations, the runtime export manifest, and the templates bundle were regenerated with the repository scripts. No wire changes, so no `HOST_DAEMON_PROTOCOL_VERSION` bump. ## How you verified Regression coverage for what the boundary actually promises — each of these fails without the corresponding behavior: - a replacement that never delegates leaves BB's renderer chunk unloaded; delegating through `experimental_Original` loads it; - the replacement receives resolved semantic props only — no parsed file, no cache key, no selection-to-chat; - with no patch text in hand the host reconstructs a single-file patch that re-parses to the same file; - crash and no-registration paths land on BB's renderer; - BB's diff renderer follows `applyResolvedCodeTheme` live — the behavior `plugins/github` previously got from a DOM MutationObserver; - `DiffFileCard`, a first-party surface, renders its text body through the same boundary; - a built-in pin renders BB's renderer with the plugin still installed and enabled, and an explicit pin survives a later plugin whose id sorts ahead of it (where automatic selection would silently swap the user's renderer). Commands run: - `pnpm exec turbo run typecheck --filter=@bb/app --filter=@get-bb/plugin-sdk --filter=@bb/plugin-build --filter=@bb/templates --filter=@bb/server --filter=bb-plugin-github --filter=@bb/cli` — passed. - `pnpm exec turbo run test --filter=@bb/app` — 367 files / 2909 tests passed. - `pnpm exec turbo run test --filter=@bb/server` — 182 files / 1730 tests passed. - `pnpm exec turbo run test --filter=@get-bb/plugin-sdk --filter=@bb/plugin-build --filter=@bb/templates --filter=bb-plugin-github --filter=@bb/cli` — 631 tests passed. - `pnpm exec turbo run lint --filter=@bb/app` — 0 errors (the repository's existing 147 react-compiler warnings; several are carried over verbatim with the extracted renderer code). - `pnpm exec turbo run build --filter=bb-plugin-github` — builds; the bundle contains `experimental_Diff` and no Pierre shim reference. **Bundle impact.** Boot payload 1655.6 → 1659.1 KB raw and 448.8 → 453.7 KB brotli against the 1671.0 / 456.4 budget; 40 → 43 chunks. I confirmed this is not new code on the boot path — stubbing out the patch reconstruction moved it by 0.3 KB — it is the extra chunks compressing slightly worse than fewer larger ones. All `forbiddenBootPackages` still pass. New lazy chunks: `BbDiff` 2.6 KB raw / 1.1 KB brotli, `BbSourceCode` 6.1 KB raw / 2.4 KB brotli. Brotli headroom drops from ~7.6 KB to ~2.7 KB, which is worth knowing before the next feature lands on that budget. **Deliberately not migrated.** Markdown fenced code blocks are genuine source but run `sugar-high` — a synchronous ~2 KB highlighter with no worker pool, no shadow DOM, and no async settle, which is what a streaming chat timeline needs. Routing them through the host would either regress that or require a second per-surface default renderer, contradicting one host per capability. Raw-log surfaces (`EventCodeBlock`, terminal output) are monospace styling, not code rendering, and are untouched. **Open questions for review**, all recorded in `docs/api_to_audit.md`: whether a per-client pin is the right scope for something as visible as every diff (and whether users expect one combined choice rather than two); whether a plugin should be able to change how *another* plugin's `experimental_Diff` renders; and whether the silent crash fallback is right where the thread list toasts. > AGENT GENERATED: by Claude Opus 5 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What was wrong
The new-thread composer treated two background discovery requests as
hard submission prerequisites. Connected-provider discovery is
host-keyed, so changing machines restarted an expensive onboarding probe
and disabled an otherwise complete composer; it could also silently
change the automatic provider choice. Project branch metadata loading
also disabled managed-worktree submission even though the create path
independently resolves and validates the default base branch on the
selected host. The composer additionally collapsed all remaining
blockers into one unexplained disabled boolean.
## What changed
Connected-provider discovery is now used only to establish the initial
automatic provider default. Its first settled result is retained across
machine switches, and the background probe no longer gates submission.
Managed-worktree submission can now proceed with `{ kind: "default" }`
while branch metadata is loading; the server resolves that default
authoritatively during thread creation. A confirmed non-Git or
commitless project source still disables managed-worktree creation, and
the branch query still enriches the branch picker.
The composer also resolves the remaining legitimate eligibility checks
into a prioritized disabled reason. The prompt submit action exposes
that reason through its accessible label and a tooltip on a
pointer-capable wrapper. This is an app-only behavior change with no
host-daemon wire, CLI, guide, or documentation changes.
## How you verified
- Added hook coverage proving that switching machines retains the
initial connected-provider selection and does not issue another
onboarding probe; this fails against the previous behavior.
- Added coverage proving that a managed-worktree request can use the
server-resolved default while branch metadata is absent.
- Added resolver and prompt-box interaction coverage for the remaining
disabled reasons and tooltip behavior.
- `pnpm exec turbo run test --filter=@bb/app --
src/hooks/useThreadCreationOptions.test.tsx
src/views/RootComposeView.test.ts
src/views/root-compose-thread-environment.test.ts
src/components/promptbox/PromptBoxInternal.test.tsx` — 197 tests passed.
- `pnpm exec turbo run typecheck lint --filter=@bb/app` — all tasks
passed; lint reported zero errors and the existing warning baseline.
- `git diff --check` — passed.
Fixes: no linked issue.
> AGENT GENERATED: by GPT-5
## What was wrong Two async transitions could suppress Edit after submitting a new thread. Provider capability gating waited on the full execution-options/model-discovery path, and navigation could briefly lose the provider facts already loaded by the composer. More importantly, the timeline controller preserved row identity using only the row ID and source-sequence range. The server projects `turnRequest.status` from `pending` to `accepted` onto that same message row without extending its sequence range, so the merge retained the stale pending object indefinitely. Edit requires an accepted message; refresh rebuilt the timeline directly from the accepted server row and made the icon appear. ## What changed The thread detail view now reads capabilities from the lightweight, environment-routed provider roster and reuses composer-warmed provider facts while that roster loads. The timeline merge also includes turn-request fields in its identity signature, so an accepted server projection replaces the pending row instead of being discarded as unchanged. Regression coverage exercises both the post-submit provider fallback and the pending-to-accepted row transition. There are no wire, CLI, guide, or protocol changes. ## How you verified - Added a timeline-merge regression test that fails before the fix by retaining `pending` and passes after the accepted row replaces it. - Reproduced the exact flow in the browser: new thread, submit, navigate to the active thread, and confirmed Edit appears without refresh while the Stop run control is still present. - `pnpm exec turbo run test --filter=@bb/client-core --force` (238 tests) - `pnpm exec turbo run test --filter=@bb/app --force -- --run src/hooks/queries/system-queries.test.tsx` (17 tests) - `pnpm exec turbo run typecheck --filter=@bb/client-core --filter=@bb/app` - `pnpm exec turbo run lint --filter=@bb/client-core --filter=@bb/app` (0 errors; existing warnings remain) - `pnpm exec prettier --check` on the changed source files Fixes: delayed edit-action visibility (no linked issue). > AGENT GENERATED: by GPT-5
## What was wrong [get-bb#2013](get-bb#2013) established the uniform rule that `input.accepted` means the provider consumed the input, never that bb queued it, because an acceptance still pending when a stale terminal arrives lets that terminal claim the input and complete an empty turn for a message the provider has not answered. Pi and ACP still emitted acceptance at dispatch. Pi's exposure is not just theoretical timing. `PiSdkSession.prompt()` resolves as soon as pi queues a prompt that arrives while a run is still unwinding, and the bridge reported that resolution as `pi/prompt/settled` — a `claimIfIdle` turn terminal. So a `turn/start` pi merely queued produced acceptance plus a terminal in the same tick, which the assembler turned into a started-and-completed empty turn while the real answer ran later under an unaccepted turn. ACP emitted acceptance in the `turn/start` handler before the turn opened, and for a steer it emitted acceptance at queue time even though the queued input is dropped whenever the turn fails or the session stops — reporting input the agent was never given as accepted into the turn. ## What changed - `PiSdkSession` tracks pending input consumption for both of pi's queues instead of steering only, and resolves it from pi's preflight hook (the input entered a run) or from the queue update that delivers a queued message. Its `prompt()` now returns that consumption signal alongside the settlement of the run it started, and reports no settlement for input pi queued into a run it did not start. - The pi bridge answers `turn/start` and emits `input.accepted` only once pi read the input. - The ACP bridge carries the waiting command with the input and emits `input.accepted` once the `session/prompt` request carrying it goes out, so the acceptance names the open turn and a dropped steer is never accepted. Every turn input still leaves with exactly one reply ([get-bb#853](get-bb#853)). - `HOST_DAEMON_PROTOCOL_VERSION` 140 to 141: older daemons emit the queue-time semantics and produce those phantom turns. Two deviations from the issue's proposed fix: 1. The issue states pi's steer path "already waits for actual SDK acceptance." It does not — `PiSdkSession.steer()` resolved once the SDK took the message into its queue, the same queue-time violation as `turn/start`. 2. Both steer paths deliberately keep answering their command at queue time, because the runtime fails a bridge request that goes unanswered for 30 seconds (`sendJsonRpcRequest`). Pi delivers steering only between assistant turns, so a steer sent during a long tool call would time out; ACP delivers a steer only when the cancelled prompt is reissued. Neither can manufacture a turn: a steer's acceptance lands in a turn the assembler already holds open, and the [get-bb#2013](get-bb#2013) failure mode needs a *pending* acceptance. Pi keeps reporting a steer its run never read through the session error path. There are no CLI, guide, configuration, or user-facing documentation changes. ## How you verified - New pi regression: a `turn/start` pi queues behind a live run emits no turn events until the queue delivers it, then the acceptance lands in the turn pi opened. Before the change it received `turn/started` + `turn/input/accepted` + `turn/completed` — the phantom turn. - New ACP regressions: acceptance is emitted immediately after the turn opens rather than before it, and a steer dropped by `thread/stop` leaves the turn with one accepted input instead of two. Both fail before, pass after. - New `PiSdkSession` coverage for queued-versus-direct dispatch, and for a queued follow-up surviving the `agent_end` that continues into it. - `pnpm exec turbo run typecheck test --filter=@bb/agent-runtime --filter=bb-plugin-provider-acp --filter=@bb/host-daemon-contract --force` — 417, 172, and 52 tests passed; typechecks passed. - `pnpm exec turbo run build typecheck --filter='...[origin/main]'` — 62 tasks passed. - `git diff --check` — passed. Fixes get-bb#2014 🤖 Generated with [Claude Code](https://claude.com/claude-code) > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## What was wrong Cursor ACP applies its project MCP approval gate to client-supplied session MCP servers. ACP has no client permission round trip for that gate, so Cursor rejected the valid `bb-bridge` stdio config before spawning it. The same config-advertisement path exists before and after get-bb#1834, and the get-bb#1932 bootstrap fix remains valid; the missing Cursor approval was the separate root cause. ## What changed The ACP bridge now installs the exact bb-owned session MCP fingerprint in the Cursor project approval store before `session/new`, `session/load`, or `session/fork`. It limits the workaround to `cursor-agent` plus the `bb-bridge` config, preserves existing approvals, serializes concurrent updates, and removes approvals that bb installed when the session ends. The MCP child also reports `initialize` back to the bridge, giving host-side diagnostics for both config construction and successful child startup. No server/host-daemon wire contract changed, so `HOST_DAEMON_PROTOCOL_VERSION` does not need a bump. ## How you verified Added fingerprint, approval-file preservation/concurrency, session-lifecycle, and MCP initialize diagnostic regressions. These expose the missing approval before the fix and pass afterward. - `pnpm exec turbo run test --filter=bb-plugin-provider-acp --force` — 175 passed - `pnpm exec turbo run typecheck --filter=bb-plugin-provider-acp` - Isolated manual run against Cursor CLI `2026.06.19-20-24-33-653a7fb`, with approval installed after ACP `initialize` and before `session/new`; Cursor spawned and initialized the MCP server Fixes get-bb#2018 > AGENT GENERATED: by GPT-5
## What was wrong Pi sampled context-window usage only on SDK `agent_end`, which fires after the entire agent run. A tool-heavy run contains multiple SDK `turn_end` events—each after an assistant response and its tool results—so bb's context meter stayed stale throughout the tool loop even though Pi's underlying context estimate was changing. See get-bb#2023. ## What changed - Sample and emit Pi context-window usage on every `turn_end`, while retaining the existing `compaction_end` update. - Stop sampling again at `agent_end`; that event is still forwarded normally for completion and checkpoint handling, but the final `turn_end` already emitted the same context snapshot. - Add a bridge regression test with an intermediate tool-result turn and a final response. It asserts that both usage snapshots arrive and that `agent_end` does not duplicate the final one. - Bump `HOST_DAEMON_PROTOCOL_VERSION` from 141 to 142 because the bundled Pi bridge's daemon-to-server event cadence changed and enrolled daemons need to update. ## How you verified - `pnpm exec turbo run test --filter=@bb/agent-runtime --force -- src/pi/bridge/__tests__/bridge.test.ts` — 27 passed. The new regression fails before the fix because only the `agent_end` sample is emitted. - `pnpm exec turbo run typecheck --filter=@bb/agent-runtime --filter=@bb/host-daemon-contract --force` — passed. - `pnpm exec turbo run test --filter=@bb/host-daemon-contract --force -- test/contract.test.ts` — 38 passed. - The full host-daemon-contract suite was also run locally: 51 tests passed and its unrelated fixed gzip-byte measurement test differed under local Node 26.3.1/zlib (`payload-size.test.ts`); the protocol contract itself passed. Fixes get-bb#2023 > AGENT GENERATED: by GPT-5
## What was wrong Long-lived threads repeatedly scanned JSON payloads for todo tool names, rebuilt the full conversation outline for unrelated command and reasoning events, and pruned arbitrarily large sets of resolved deltas in one synchronous SQLite write. Those paths blocked the server event loop and delayed otherwise small event inserts. Separately, stall diagnostics attributed awaited RPC wall time as event-loop work, treated laptop suspension as a runtime stall, and warned on fresh 512-event bursts before there was evidence that delivery was stuck. ## What changed - Add a guarded generated tool-name column and partial todo lookup index through Drizzle migration 0104. - Key the conversation-outline cache by the latest outline-relevant event while still returning the current thread sequence. - Use the materialized parent-tool-call column in remaining event queries and cap each resolved-delta prune pass at 500 rows. - Attribute event-loop stalls only to completed synchronous work; keep awaited routes visible only as current work. - Reset server and host event-loop samples after likely system suspension and report the host heartbeat wake as informational. - Require a depth-512 daemon event queue to remain queued for five seconds before warning, while retaining the unconditional thirty-second age warning. The timeline byte limit and default event budget are intentionally unchanged. There are no server/host wire changes, so HOST_DAEMON_PROTOCOL_VERSION is unchanged. ## How you verified - pnpm exec turbo run test --filter=@bb/db --force: 406 tests passed. - pnpm exec turbo run test --filter=@bb/host-daemon --force: 586 tests passed. - Affected server suites: 91 current tests passed, including outline caching, event-loop attribution, and timeline-window regression coverage. - pnpm exec turbo run test --filter=@bb/config --filter=@bb/domain --force: 108 config and 137 domain tests passed. - Affected app tests passed: 43 tests. - pnpm exec turbo run typecheck for @bb/db, @bb/domain, @bb/config, @bb/host-daemon, @bb/server, and @bb/app: all passed. - Reproduced the todo lookup on a copied 41k-event thread: median 11.46ms to 0.04ms. Reproduced a 5k-delta prune: median 10.05ms to 1.22ms per bounded pass. The full server suite was also attempted, but existing npm-artifact packaging tests do not produce a clean signal in this sandbox; all suites covering changed server paths passed. Fixes: N/A — log-driven performance investigation. > AGENT GENERATED: by GPT-5
## What was wrong Session-open validation required the newer `localApiPort` field before comparing daemon and server protocol versions. Daemons from before that field existed therefore received `400 invalid_request: Required` instead of `protocol_version_mismatch`; because the daemon only invokes its protocol self-updater for the latter response, an enrolled older daemon could retry forever while the server reported it offline. ## What changed - Default a missing `localApiPort` to `null` at the server boundary so pre-field session payloads reach the protocol-version check. - Keep the current daemon-side request type explicit by exporting the schema's parsed output type. - Add a regression request frozen to the pre-`localApiPort` wire shape, which protects future required session fields from bypassing the mismatch response. - Bump `HOST_DAEMON_PROTOCOL_VERSION` from 142 to 143 for the wire-boundary behavior change. ## How you verified The new server regression reproduces the old daemon payload without `localApiPort` and now receives `protocol_version_mismatch`; before the fix, the live equivalent received `invalid_request: Required`. - `pnpm exec turbo run test --filter=@bb/host-daemon-contract --force -- --run test/contract.test.ts` - `pnpm exec turbo run test --filter=@bb/server --force -- --run test/internal/internal-session-protocol-version.test.ts` - `pnpm exec turbo run test --filter=@bb/host-daemon --force -- --run src/server-client.test.ts src/protocol-self-update.test.ts` - `pnpm exec turbo run test --filter=@bb/scripts --force -- --run test/request-dev-restart.test.ts` - `pnpm exec turbo run typecheck --filter=@bb/host-daemon-contract --filter=@bb/host-daemon --filter=@bb/server` - `pnpm exec prettier --check packages/host-daemon-contract/src/session.ts packages/host-daemon-contract/src/protocol.ts packages/host-daemon-contract/test/contract.test.ts apps/server/test/internal/internal-session-protocol-version.test.ts` - `git diff --check` Fixes: N/A (no linked issue). > AGENT GENERATED: by GPT-5
## What was wrong The active thread timeline and the full-history conversation outline shared the same realtime invalidation group, so every events-appended streaming batch sent another /conversation-outline request. The server cache hardening now on main from get-bb#2025 avoids rebuilding for outline-irrelevant events, but the client still performs redundant HTTP reads at streaming cadence, and assistant text deltas can still invalidate the full projection. The outline does not need sub-second route refreshes because the incremental timeline already carries the live conversation rows. ## What changed - Split realtime timeline-window invalidation from conversation-outline invalidation. - Refresh the full outline at the terminal turn boundary instead of for every streaming delta; unknown lifecycle notifications still invalidate it conservatively, and history rewrites retain the existing full invalidation path. - Overlay live timeline conversation rows onto the cached full outline so current user and assistant messages remain fresh while a turn streams. - Reconciled the server cache documentation with the outline-aware cache added by get-bb#2025 and the new client refresh policy. - Added regressions proving streaming deltas do not refetch the outline, turn completion does, and live timeline labels replace or extend a cached outline. This implements the client-side pacing direction from get-bb#1972 using a turn boundary plus live-row overlay instead of a timed debounce. It does not change the API contract or the server/daemon wire format, so HOST_DAEMON_PROTOCOL_VERSION is unchanged. ## How you verified The new realtime invalidation test fails before the change because an assistant delta refetches the active outline query. The TOC merge test also fails before the change because a loaded outline always wins over newer timeline rows. After rebasing onto origin/main at 0b2723a: - pnpm exec turbo run test --filter=@bb/app -- src/hooks/cache-owners/cache-owner-registry.test.ts src/hooks/realtime-cache-effects.test.ts src/components/thread/toc/ThreadTableOfContents.test.tsx — 80 tests passed - pnpm exec turbo run typecheck --filter=@bb/app — passed - git diff --check origin/main...HEAD — passed Fixes get-bb#1972 > AGENT GENERATED: by GPT-5
The edges kept disappearing because every ring form fails here for a different reason: an OUTSET shadow is clipped away by the dissolve mask, and an INSET shadow paints beneath child backgrounds, so the chrome bars and rows erased it. A border belongs to the element's own box — children sit inside it, and the mask keeps it — so the frame finally holds its left and right edges. Written as longhand border-width/style/color: the shorthand with a color-mix value was being dropped, computing to border-style: none. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rest-and-respond rewrite dropped the .in reveal class from board cards but left the rule that hid every card without it, so the board rendered three empty columns.
The nested rows carried a 2px rounded border-left stub per child. The app draws one 1px hairline down the whole child group and sets children apart by a 24px indent alone, so the demos now do the same. Also corrected against the shipped sidebar: finished threads end in the 5px unread dot, never a green check; rows are 28px with a 6px radius and 8px lead instead of 36px and 8px; hover and selected carry the app's own --sidebar-accent and --state-active strengths rather than half of each; and project labels take the chrome label recipe. Dropped the invented 'reported back to its parent' row, which has no counterpart in the app. Headings that had drifted onto one-off oklch literals now read from --ink-strong with the h1.
Both were the weakest demos on the page and failed the same way: a strong left column beside a thin right one, with a skeleton panel hanging off the build frame and a chrome-less rail standing in for a window in spawn. Each is now one window, on the chrome the gang demo already proved. Build is a thread: you watch the ask land, the agent scaffold the plugin, register the CLI and write the skill, and then the panel it built appears in the sidebar nav beside you. That arrival is the section's whole claim, and it now happens in the sidebar rather than in a floating preview. Spawn stops floating its causes outside the frame. A shell command, a Telegram message and a nightly automation are threads, so they arrive as threads, each carrying the glyph the app gives a background spawn and a transcript that opens by naming where it came from. Clicking a row opens it, as it does in the app. Thread panes are bottom-aligned now, because a thread you are looking at is scrolled to the bottom. That is both truer and why neither pane carries a band of dead space under its last line any more. Copy corrected against the shipped thread: the composer says 'Ask a follow-up', and the diff chip reads 'Uncommitted · N files' without the invented 'Working tree' prefix.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f5825f8d2
ℹ️ 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".
| .side-row-new /* Only the New-thread button shares its row (with the search chip); the | ||
| plugin rows are their own lines and must not stretch. */ | ||
| .side-row-new .side-act { |
There was a problem hiding this comment.
Fix the selector so the new-thread action can flex
The intervening comment turns this into .side-row-new .side-row-new .side-act, which never matches the JSX because the button is an immediate child of a single .side-row-new. Consequently, the intended flex: 1 is never applied, so the New thread action shrinks to its content and the search affordance is not aligned at the far edge of the sidebar row.
Useful? React with 👍 / 👎.
68 rules and three keyframes for the old two-column build and spawn compositions, plus the selectors that could only ever match their state classes. Verified by diffing the rendered page: every static region is pixel-identical and the page height is unchanged.
## What was wrong The mobile thread header showed a "Working" subtitle with a spinner under the title while a thread ran. The timeline already shows a working indicator, so the header line was noise. ## What changed - `apps/mobile/src/screens/thread/ThreadDetailHeader.tsx`: `headerSubtitle` hides working-tone statuses (Working, Provisioning, Starting, Stopping, Reconnecting). The header keeps "Needs input", "Error", "Waiting for host", "Archived", and the child / side chat label. The spinner is gone. - `apps/mobile/src/screens/thread/thread-detail-header-model.ts`: removed the unused `spinning` field from `ThreadStatusPill`. ## How you verified - `pnpm exec turbo run typecheck --filter=@bb/mobile` passes. - Manual check in the iOS simulator against the mobile e2e backend: an active thread shows only the title in the header, and the timeline still shows "Working...". Fixes # > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong The iOS unread divider used the `attention` token (yellow), a centered label, and two rules. The web app uses `timeline-accent` (blue), a left label, and one rule. The two apps did not match. ## What changed `apps/mobile/src/screens/thread/timeline/TimelineList.tsx`: the divider now uses `text-timeline-accent` / `bg-timeline-accent`, an uppercase medium-weight "New" label on the left, and one rule on the right. This matches `UnreadDivider` in `apps/app/src/components/thread/timeline/ThreadTimelineRows.tsx`. ## How you verified `pnpm exec turbo run typecheck --filter=@bb/mobile` passes. Visual change only; no new tests. > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude <noreply@anthropic.com>
…2034) ## What was wrong The mobile composer's voice bar (`apps/mobile/src/composer/VoiceBar.tsx`) showed a red dot, a "Listening…" label, and an elapsed timer. It gave no live audio feedback and did not look like the web `VoiceRecordingBar`, which draws scrolling sound-wave bars from the microphone level. ## What changed - `apps/mobile/src/composer/voice-waveform-model.ts` (new): pure port of the web `WaveformVisualizer` math. `meteringToAmplitude` converts expo-audio metering (dBFS) to a bar amplitude with the same noise floor, gain, and gamma as the web RMS path; plus the scrolling bar buffer and the SVG path builder. - `apps/mobile/src/composer/VoiceWaveform.tsx` (new): draws the bars as one `react-native-svg` path (3px bars, 2px gaps, round caps, newest at the right, oldest fading on the left via a gradient stroke). Samples `readLevel()` at ~30 Hz while active, freezes when inactive, shows flat idle bars under reduce-motion. - `apps/mobile/src/composer/VoiceBar.tsx`: web layout — round ghost cancel · waveform · round primary confirm. While transcribing the bars freeze and breathe (the `animate-shine-icon` stand-in) and the confirm button shows a spinner. - `apps/mobile/src/composer/useComposerVoice.ts`: records with `isMeteringEnabled: true` and exposes `readLevel()`; the elapsed-seconds ticker is removed. - `apps/mobile/app/dev/ui.tsx`: a "Voice bar (synthetic levels)" gallery section so the bar can be exercised without a mic. No wire changes. ## How you verified - New `voice-waveform-model.test.ts` (dB mapping floor/clamp/monotonic, scroll buffer, path geometry). `pnpm exec turbo run test typecheck lint --filter=@bb/mobile` pass. - iOS Simulator (iPhone 17 Pro) through the dev client and the UI gallery: recording scrolls right→left with the left-edge fade; Check → transcribing freezes and breathes with a spinner; X → recording resumes. Checked dark and light. Fixes # > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude <noreply@anthropic.com>
## What was wrong The mobile app's left drawer (`expo-router/drawer`) repeated the home screen: home already shows the grouped thread list with the compose dock. The drawer only added the server switcher, Settings, display options, and a path to another thread from inside a thread. It cost an edge gesture and a scrim, and the bb connect edge-swipe press-through (noted in the Phase 7 integration entry) came from it. ## What changed - `apps/mobile/app/(drawer)/` and `src/screens/shell/DrawerContent.tsx` are removed. `app/index.tsx` (home) is the root of the native stack; `_layout.tsx` anchors on `index`. - New `src/screens/shell/WorkspaceMenu.tsx`: the home header's left button is the active server's initials with the realtime dot. It opens a bottom sheet with the server label and connection state, the server rows, Add server, Archived threads, Settings, and UI gallery in E2E mode. It dims with the compose scrim. - `HomeScreen.tsx` sets the title (server label) and the header-left button in every ready state. Search and display options stay in the header's right slot. - Dead code removed: `SidebarActionsProvider.onBeforeNavigate`, and `selected` on `SidebarThreadList` / `SidebarThreadRowView` (only the drawer highlighted the open thread). - E2E: new `e2e/subflows/open-settings.yaml` (avatar → Settings) replaces every `drawer-*` step in 10 flows. `phase1-shell` asserts the sheet contents, `phase3-threads` searches from `home-search`, `phase5-connect` drops the header-toggle workaround. - Docs: `apps/mobile/README.md`, a new entry in `plans/bb-mobile-progress.md`. No wire changes. ## How you verified - `pnpm exec turbo run typecheck lint test --filter=@bb/mobile`: green, 817 tests (adds `workspace-initials.test.ts`). - Maestro on the iPhone 17 Pro simulator (iOS 26.3) against the e2e harness: `phase1-shell` and `phase3-threads` pass end to end. Screenshots show the avatar in the header and the workspace sheet. - `phase7-settings` was not run: its pre-flight `phase7-settings-reset.js` got a 400 from the harness before the app launched (unrelated to this change). > AGENT GENERATED: by Claude Opus 5 Co-authored-by: Claude <noreply@anthropic.com>
The showroom prose still described a task tracker while the demo built a review queue. The demo changed and the copy did not follow, so the section argued with itself. Copy now names what the demo actually ships. Rendering law: - The hero and nav entered from opacity 0 with a 6px blur, so the page's claim did not exist for the first 120ms — in a screenshot, for a crawler, or as the LCP element. Both entrances are transform-only now and the headline carries no delay. - Stat numerals were zeroed at hydration and held that way until the reader scrolled to them, up to five seconds. A full-page capture could record four zeros about a project that has none. The roll is armed on intersection instead, so the true value is on screen at every other moment. Mobile: - The bento's mobile rule set min-height while the desktop's fixed 440px height stayed in force, so the intended reduction never happened. - Three responsive rules set grid-template-columns on the demo roots, which are flex containers. The grid is on .gang-body; they were no-ops. Accessibility: - The PR marquee paused for the mouse but not the keyboard, and its rows are real links. Pauses on focus-within now, with a visible focus ring. - Two spinners were missing from the reduced-motion block. - The copy control's checkmark had no announced equivalent. - Company names were 14px at 55% opacity because the whole item was faded. The mark is faded now; the name is not. Also removed the last two hover lifts, on the release pill and the provider marks, neither of which is a control.
… layout The toggle rearranges twelve tasks; it was blinking between two renders of the same data. Matching view-transition-names on each card and its row let the browser move them, with the root cross-fade switched off so a local toggle does not dissolve the page around it. No dependency: where the API is missing, or motion is not wanted, it is the same instant swap as before. Also from the compositing and typography passes: - The hero's Changes pane animated its own width from 0 to 320px, running layout on the largest object above the fold every frame and reflowing the thread column with it. The box is final from the first frame now and only its contents travel. It also joined on a mount timer, so it appeared in neither the prerendered HTML nor any capture taken before the timer; it is open in the markup and CSS owns the width gate. - The subscribe field was 14.5px, which makes iOS Safari zoom the whole page when it takes focus. 16px, rather than capping maximum-scale and costing pinch zoom in every other browser. - will-change on the PR marquee, the one element running an uninterrupted transform loop. - font-optical-sizing on Inter's opsz axis, font-synthesis: none, a themed ::selection, real underline offsets, and user-select off on demo chrome so a select-all takes the page's words and not the scenery's.
- Removed .closer-room. The bordered box was fencing the last act: ~258px of its 550px was border and padding around 292px of content, and it boxed the final CTA immediately before another bordered signup card. The closer is one open centred column again, with 128px of approach above it. - The closer install card takes the same 660 measure as the hero's, so the repeat is the same artifact rather than a squeezed copy. - The subscribe card takes the rail, like the stat cards and PR feed above it; its form keeps its own 560px measure and stays centred. - The copy well sits flush to the command button's right edge. The 10px that held it short was also squeezing "npx bb-app@latest" into an ellipsis at every width. - Subscribe is disabled until the address parses, so the "Enter a valid email address" state can no longer occur. The error slot stays for genuine request failures -- a subscribe that silently does nothing is worse than a message. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
36px against a 42px input left the pair sitting on two different rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…glyph - .threads stacked its rows flush at 0px while the other three demos all carry the app's 2px separation; the hero rail was the only outlier. - The $ is a shell prompt, so it sits exactly one character of the button's own monospace to the left of `npx` instead of being spaced off like the Apple mark in the button beside it. Measured: 9.02px against a 9.03px character. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two row primitives the landing had collapsed into one, plus the theater review's findings. rows - Thread rows are 28px, not 32. The app has two primitives: ThreadRow uses --bb-sidebar-row-height (1.75rem, theme.css:553) while SidebarMenuButton's h-8 covers project/menu/footer rows. Added --thread-row-h and applied it to .trow and .sub-row only; menu rows keep 32. - Consecutive thread rows sit 1px apart (space-y-px, ProjectRow.tsx:1141). The hero rail was stacking them flush at 0. steps - "Edited ProjectList.tsx" is expandable now, as a real <button type="button"> with aria-expanded, a hover-revealed 12px chevron that rotates 90deg, and a 0fr/1fr grid body over 200ms ease-out -- the app's own disclosure, not an approximation of one. "Read ProjectList.tsx" stays a static div, because the app does not expand exploration-intent rows either. - The hero was the only demo drawing a bare chevron as its leading glyph; it now uses the same work-kind chooser the other three already had, at the app's 14px, on a 20px line. theater review - User messages rendered on the assistant side. The app right-aligns them at a 70% cap; left-aligned at 94% the transcript read as one undifferentiated column. Now flex-end at 70%. - The composer carries shadow-lift, cast upward, so it reads as bottom-anchored material rather than another outlined card. New --shadow-composer, derived from the anchors, with a dark override. - The diff toolbar is a 56px band and now states the change set it is showing. The count is derived from DIFF_LINES so it cannot drift from the fixture. - The logo rail was 1320 against the mock's 1200, protruding 60px each side directly beneath the page's largest artifact. Both are 1200 at x=85 now. Left open: the responsive mock still invents a pane carousel at 1099px and scales with zoom below 720px, which puts 8.7px text on a phone. That one is a composition decision, not a defect fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…above Rendering law: .tg-msg animates with `backwards` fill and inline delays out to 2.9s, and the reduced-motion block that should have caught it was empty. A reduced-motion visitor, a JS-off prerender, and any single screenshot lost the spawned-thread card -- the entire payoff of "Text it to work". The bubbles, the swap, the spinner and both status glyphs now settle into their finished state, so the conversation is complete and the thread reads "running" at rest. Token law: .tg-bubble and .tg-thread carried a raw rgba(0,0,0,0.05) shadow. The documented Telegram brand exception covers that component's own surfaces, not its elevation, and pure black at 5% is invisible on the dark scheme's bubbles -- so the lift existed in light and vanished in dark. Both derive from --shade now. The two phone insets had a border and a ground but no drop-shadow, so the top row read as two lifted windows and the bottom row as two flat panels. The chat inset takes the same three-layer ramp .bento-window uses. Note: the explanatory comment sits above the @media block rather than inside it. As its first child it made Lightning CSS fail with "Missing opening {". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… room The ask-question options overlapped. `.askq-opt` was pinned to `height: var(--row-h)` while holding a title over a description, so each option's second line ran under the option below it. Collateral from an earlier blanket min-height -> height conversion, which was right for single-line rows and wrong here. Swept the rendered page for siblings of the bug; the only other overflows are heading descenders, the sr-only clip, and the demos' own masked dissolves. No overlay in any demo dismissed. There was no pointerdown, mousedown or document listener anywhere in the file. Added one useDismiss primitive and wired every transient surface to it: the thread-actions menu, the editor menu, and both sidebar searches. It listens on pointerdown, not click, because that is what Radix does and it is why a dismissed menu feels dismissed rather than toggled -- the surface is gone before the control under the pointer gets its own event. Escape closes too. Inline title rename already committed on blur and cancelled on Escape, which is the app's convention, so it is untouched. Verified: each menu opens, closes on an outside pointerdown, closes on Escape, and stays open on a pointerdown inside itself. Also: - All four bento cards take one light slate; the alternating deeper step for cards 2 and 3 is gone. - The signup card is the section's own ground rather than a contrasting panel on it. - The closer returns to a bordered room, on the rail this time, so it aligns with the stat cards, the PR feed and the signup at 1200 instead of sitting at the install card's 660. The closing band never carried .band's 96px because it is not a .band; it does now, so the room has real approach from the section's top edge. - Hero economics line tightened to "Free · MIT · Local-first · No subscriptions". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Honesty law: RunCommandButton set copied=true and emitted landing_cli_command_copied before writeText() resolved, then discarded the rejection. In an insecure context or with permission denied it announced "Install command copied" and recorded a copy that never happened -- a false claim to the user and a false metric. The write is awaited now; on rejection it announces "Couldn't copy. Select and copy npx bb-app@latest." and emits nothing. - .install-head was 34px against the 32px row primitive. - At 720px the action pair collapsed to a single column, giving two 604px-wide buttons on a tablet. An intrinsic repeat(auto-fit, minmax(min(250px,100%), 1fr)) keeps two equal columns wherever they fit and stacks where they don't, inside the existing tier rather than adding a fifth breakpoint. Verified: 604px content -> two 297px columns, 334px -> one. - The logo was the only nav control under 44px on a phone (36x36). The coarse rule grows the hitbox only; the mark stays 36px and gains no plaque. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ubble - .review-demo is a fixed-height flex column, so an expanding file card took its height out of the card above it: opening one file visibly shrank the diff already on screen. Cards are flex: 0 0 auto now and the column grows into the demo's own clipped overflow. Verified: the first card holds at 160.7px while the second goes 33.5 -> 128.7. - The incoming Telegram bubble had no border at all, only a 1px shadow, while the thread card beneath it carried a real border plus the same shadow -- which is why the bubble read as faded next to it. The outgoing green bubble is a filled brand surface and stays borderless. - Removed the "Tap an answer and the thread carries on." hint, and its two dead selector entries. Both lists keep another selector (.ap-opt, .ap-reply) so no declaration block was orphaned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- The unread dot was derived from "is not the currently open thread", so every thread you left re-armed itself. No real client does that. Reading a thread now marks it read for good; the dot is unread state again. Verified: three threads opened and left stay cleared while the rest stay dotted. - A real <select> opens the OS popup however the closed control is styled, so clicking the diff scope painted a macOS menu on top of a recreation of bb -- the one place OS chrome cannot appear. That control drove no state, so it is the label it always was. Three functional selects remain in other demos and have the same exposure. - The install command is a label you copy with the button beside it, not text to drag-select. - Fixed the regression from pinning the review cards: .review-demo had min-height:auto, so a pinned card pushed the column and the column pushed the bento tile, resizing it on every expand. It takes the window's height and absorbs its own growth now. Verified: the tile holds at 440px through expand and collapse. - "More than a chat window" -> "More than a chat". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving the panel's padding down to .build-queue for the 48px header seam left its sibling caption with a 2px inset, so on mobile it sat flush against the card's edge and read as escaping it. It takes the panel's inset now and lines up with the queue rows at 15px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dark-mode "glow" behind the model dropdown was macOS drawing its own popup -- system shadow, system blue highlight -- on top of a recreation of bb. `appearance: none` only ever styled the closed control; the popup is the OS's and cannot be styled at all. The picker is now the app's own: a button with a chevron and a menu this page owns, dismissing on outside pointerdown and Escape through the same primitive the other menus use. Worse than the popup: Attach called attachmentInputRef.click(), which opens a real macOS file-chooser from a marketing page. Attach, voice and send are scenery now -- spans, aria-hidden, pointer-events none -- wearing the app's shape and doing nothing, alongside the composer status they used to drive. Verified: zero file inputs on the page, the picker opens/selects/closes, outside pointerdown dismisses it. Two functional selects remain, in the extensions filter and the review scope, carrying the same exposure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last two functional <select>s -- the extensions category filter and the review scope -- carried the same exposure as the model picker: the OS paints its own popup, and appearance:none only ever reached the closed control. Rather than a third copy of the pattern, extracted DemoPicker: a button with a chevron over a menu the page draws, dismissing on outside pointerdown and Escape through the shared useDismiss primitive, with drop="up" for the one that sits at the bottom of the composer. All three pickers are on it now. Verified: zero <select> elements on the rendered page; each picker opens, selects and closes, in its correct direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hero's sidebar toggle set `hidden` on the aside and narrowed the header
from 320px to 48px -- and the rail stayed exactly where it was. Root cause:
`.side { display: flex }` beats the UA's `[hidden] { display: none }`, because
author styles outrank the UA sheet, and 7,000 lines of stylesheet had no
`[hidden]` rule at all. The attribute was inert on every styled element on the
page. Now it isn't.
Verified: the rail collapses to 0, the thread pane reclaims exactly the 320px
it gave up (748 -> 428), and reopening restores both precisely.
The build and gang windows showed a rail with no way to collapse it. Both have
the hero's toggle now, at the left end of the chrome row where the app puts
it, and their grids hand the column back rather than holding a 320px gap.
Verified: build 320 -> 0 (three columns to two), gang 320 -> 0 (two to one),
both restoring.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Below 1099px the window became a horizontal scroll-snap carousel whose panes were pages, and below 720px it was scaled with `zoom`. Neither is anything bb does: the app stays inline to its 767px compact boundary (use-compact-viewport.tsx:10-36) and then uses drawers. Measured cost of the old behaviour: at 900px the scroller put main/sidebar/diff at x=29/839/1649, so two thirds of the product sat off-screen; at 390px `zoom: 0.646` rendered a mandated 32px row at 20.7px and body text near 8.7px. The panes now drop the way the app drops them -- rail at the compact boundary, changes pane at two-up -- and below 720px the window keeps its real 980px and runs off the right edge under a 48px dissolve. A legible slice of the product beats an illegible whole of it. Deleted useFitMock, --mock-scale and --mock-visible-width; no fifth breakpoint was added. Verified: zoom 1, body text 13.5px, step rows 20px, no page-level horizontal scroll, and zero scroll-snap rules left anywhere in the stylesheet. Laws 1, 4 and 5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rnal Behaviour was fixed earlier; geometry had never been checked. Measured against the real prompt box and its shared-ui primitives, then corrected: - The picker was animating a background over 160ms. The app's pickers carry LIST_HOVER_TRANSITION -- `transition-none` -- and do not animate at all (option-display.tsx, motion.ts:22). That was motion the product does not have, so it is removed rather than retuned. - Picker geometry to the OPTION_BASE contract: padding 8px -> 4px (px-1), gap 7px -> 4px (gap-1), font 13px -> 12px (text-xs), muted -> foreground on hover. - Glyphs inside the picker and the send button to 16px. Every SVG inside a shared-ui Button is painted `size-4` by the cva base whatever the call site authors, so the landing's 15px and 12px were both off. - The context row is locked to 24px, 4px below the box, inset 15px/14px with 8px between chips, and clips rather than wrapping -- it was 26px tall on a 10px margin with 4px gaps, and wrapped to 47.2px across two lines. - Chips to h-6 (24px) and text-xs. - Editor text to 13px/1.7, from 13.5px/1.5. Verified: zero remaining drift across seventeen measured properties, no page-level horizontal scroll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e card The gang section carried a second copy of the hero's provider rail. The claim directly above it already names all eight providers in prose, and the demo now shows a different one per thread, so the logos were saying a third time what the section had already said twice. Removing it left `.act-lead .providers` and `.band-copy .providers` (and its narrow-screen override) matching nothing. Each was a standalone block, checked against the DOM before deleting so no shared declaration was orphaned. The signup pair takes the install card's 660px measure, so the closer's two acquisition surfaces line up: input 548.3 + 12 gap + button 99.7 = 660, left edges flush with the card above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SpawnDemo renders its rail rows as <button>, in a demo the selection rule never named, so it silently kept two bugs the other three had fixed: no visible selected row, and an unread dot derived from `open.id === c.id` that re-armed every time you left a thread. The cause was specificity. `.sub-demo a.sub-row` (0,2,1) outranked `.sub-row.is-open` (0,2,0), so the state rules had to be rewritten per demo and per element type -- and a fourth demo using a different element fell straight through. The scope now sits in :where(), which contributes no specificity, so the state rules win on their own and apply to every rail row anywhere. Verified across all four demos: the selected row paints oklab(0.3211 0 0 / 0.118) in each, including SpawnDemo's button rows. SpawnDemo's unread state is modelled like SubagentsDemo's -- read once, read for good. This is a patch, not the fix. sol is auditing for the shared primitive that makes the whole class impossible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Blog had four different left edges on one page -- nav and footer on the 1200 rail, the post index at 924, the post column at 680, and the signup form at 660 aligning with nothing. Changelog disagreed again: its release bodies were 686 and its page head took the full 924. The 660 was mine, from this morning: I sized the signup to match the closer's install card, and it leaked onto both content pages where no install card exists. It is scoped to `.closer-subscribe` now, where the reason for it is. Added `--prose: 680px` beside `--rail`, and put blog posts, the blog page head, changelog release bodies and the signup on every content page on it. The two pages now agree with each other and with the landing's rail rather than each picking a number. Verified at 1440: blog index page-head / post / form all x=974 w=680; post detail article-head / post / form the same; changelog release body and form both 680; nav and footer 1200 on all three, matching the landing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
My earlier pass invented `--prose: 680px`, which was both the wrong width and a duplicate: the stylesheet already declares the ladder (`--rail` 1200, `--stage` 1440, `--measure` 640) with a comment saying sections use these and never bespoke widths. Removed it. Blog, the post page and the changelog now put their heads, bodies and releases on `.rail`, so every block on those pages shares the 1200 the landing's demos, cards and PR feed already use, and the nav and footer they were already using. The signup is the landing's own SubscribeCard on all three pages instead of a bare bordered strip -- same container, same 660 form centred inside it. The card takes an optional description and id so each page keeps its own line and its #subscribe anchor. Verified at the same viewport on all three: page head, body, signup card and footer all report the nav's exact x and width, with the form at 660 inside the card and no page-level horizontal scroll. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ference The agent stat was a floor presented as a measurement. It came from a full-text search for "AGENT GENERATED", a tag upstream asks agents to append -- so any agent PR without the line was uncounted, and "of those, written by agents" claimed exhaustive detection the method cannot provide. The per-row chips had a second problem on top of that: the feed filters out lnittman, who writes most of the human PRs, so 17 of 18 visible rows carried a chip against a 60% aggregate. A label on 94% of rows distinguishes nothing, and each row asserted individually what the count asserted better. Both removed, along with the second GitHub search, the `agent` field on feed rows and `agentMergedLastMonth` -- otherwise the build kept fetching data nothing rendered. Stat grid is three columns. Hero type set to the Paper reference: 64px / wght 510 / -0.022em / 100%, on Inter Variable, replacing 72px / 640 / -0.045em, which was heavier and tighter than the mark it aimed at. The lead follows at 15px / 160% / -0.011em. Verified computed: 64px, "wght" 510, -1.408px, 64px line-height. Blog's page head is centred on the landing's own treatment, its sub on the same .section-lead values. Changelog release prose lost its 38em/44em caps: they were sized for a 686px column and, once the body moved to the rail, they -- not the content -- were deciding where lines broke. Verified: li now 962 wide, matching its column. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Get new posts by email" and its changelog twin were bare text links doing a call-to-action's job. They keep the anchor -- a real in-page target that works with JS off, which is how the landing's own GitHubLink is built -- and take the btn btn-ghost recipe: 39.4px, 6px radius, 14.5px/520, 16px glyph on an 8px gap. The generic .meta-row link colour is scoped :not(.btn) so it stops fighting the button's own treatment. Reverted the centring from the previous commit: the blog head sits flush left like the changelog's, with h1, lead and button all on the same edge. The lead keeps the landing's .section-lead type (15px / 160% / -0.011em) -- that part was cohesion worth having, the centring was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The closer room takes the changelog media stage's ground -- the same 22px radial-dot pitch at the same 12% ink -- so the last room on the page sits on the surface its demo cards do. RELEASE_MEDIA already existed as a version-keyed map with one entry. Filled three more, each drawn only from what that release's own notes claim: - 0.35.0 Plugins: plugin pages as flat sidebar rows with Automations separate from Extensions, which is what those notes describe. - 0.34.0: the cross-provider Ask User Question card, built from the landing's own .askq so it matches the recreation on the homepage. - 0.0.31 Splits: three panes side by side, the active one carrying the app's selected surface. The release says up to eight; three is what the card fits and the prose beside it carries the number. New .split-card and .ask-card styles reuse the existing media-card shadow and the 48px chrome row. The pane title needed its own box for the ellipsis -- as a bare flex child it hard-clipped mid-word. Four of ten releases now carry a visual, all on real surfaces. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plugins and skills listed with their install state, on the app's own metrics: a 48px chrome row over 28px rows, verified. The names, the plugin/skill split and the install affordance all come from that release's own notes -- nothing depicted that the changelog does not claim. Five of ten releases carry a visual now. The remaining five are model-catalog refreshes and fix rollups with no surface worth drawing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The label already says macOS; the mark was restating it. Removed the hand-authored AppleSolidIcon with it, since that was its only call site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four terms in four slate squircles instead of a middot-separated run. The dots were doing the separating; the tiles do it now, so they go. `corner-shape: squircle` is a real superellipse where the browser supports it (Chrome does, verified computed) and degrades to the 10px radius everywhere else, so no fallback branch is needed. Ground derives from --ink-strong at 5% over the canvas with the shared --ring edge; 26px tall on a 6px gap, centred and wrapping. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sol's steps 1-2 of the seven-step parity refactor, verified here before landing: - demo-app-primitives.tsx: DemoThreadScene (monotonic read-through reducer -- selection can never re-arm an unread dot, by construction), DemoThreadRail (32px project rows, 28px thread anchors, 1/2/4px spacing, file-private row renderer: openable models become anchors, scenery becomes inert divs), chrome/action-row/glyph primitives with a 12|14|16 glyph type. - One marked canonical CSS block owns every .demo-* selector; state rules are single and unscoped. - Two enforcement suites (10 tests): reducer/status-priority/SSR contracts, and the CSS contract -- canonical-block ownership, token-sourced metrics, exactly one selected and one hover rule with no demo names or tag-qualified arms in either. - Subagents is migrated. Verified live: project rows 32px, thread anchors 28px, parent-to-child 2px, one painted selection, aria-current on the open row, and A/B/A leaving both threads read. Gates: build, typecheck 0, 89 tests (was 79). Of note from the run itself: the delegate wrapper died with its parent shell, so its fallback lane re-spawned the same brief on pi over the finished work; it was stopped before writing. The codex session's own result file carried the report the wrapper lost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The felt-quality interview picked "roughness" as a twin failure; this is the measured pass over page chrome. Verified computed before and after; the demo surfaces were left to the primitive migration. - .sec-title carried the page's only 700, with the body's 1.6 line-height leaking onto a display heading. It joins the display family (64/510 -> 54/600 -> 32/600) at 1.1. - The signup title had the same line-height leak: 22px on 1.6 -> 1.15. - The stat numerals sat alone at 620 between the display 600s and nothing; now 600. (.ext-detail's 620 is demo territory and stays for the migration.) - The hero tiles were 10px, off the 6/8/12/16/20 ladder -> 8px, squircle kept. - The hero's two large breathing gaps (callout->h1, tiles->providers) were 48 and 44; both 48 now. Checked and deliberately NOT changed: the --act *0.6/0.45/0.35 band system, the stat-card inset top-light, the four-step ink ladder, 480/550 label weights, and the callout's stronger border on its canvas ground. Zero 700s remain in chrome. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ransitions The felt-quality direction is still chrome, living product. The chrome half: - Nav entrance (hdr-down) and the hero's staggered fade-up are gone -- removed, not replaced, per the review that called them marketing-animation language and the owner's stillness call. Their reduced-motion escapes go with them. - The button press scale (0.985) is gone with its --duration-press token; the app's buttons are colour-only. Buttons keep 0ms-in/150ms-out background/ box-shadow/color. - Provider marks are bare SVGs, not controls; their hover ink shift and transition were invented motion. Removed, including their entries in the two shared selector lists -- each list kept its other members (checked; this is the surgery that once broke the hover law). And a real bug the measurement kept tripping over: applyThemePreference sets data-theme-switching to kill every transition for one frame, removing it in a requestAnimationFrame -- which never fires in an occluded or backgrounded document. React effects still run there (MessageChannel, not rAF), so the attribute was set and never cleared, leaving the whole page's transitions dead. A 120ms timeout now backstops the rAF; whichever fires first wins. This was also the source of every "all transitions are none" misreading this session. Verified: attribute clears on load, hero and nav report zero animations, buttons at color-only 150ms, hover law intact. 89 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sol's step 3, verified here before landing. Both demos now render their rails through DemoThreadScene/DemoThreadRail: 28px thread anchors, 32px project rows, 1px siblings, 2px parent-to-child, 4px between projects -- each delta app-cited in the report. The one behavioral catch: Gang suppressed status glyphs on the selected row (`open.id === thread.id ? null : status`), so a running thread's spinner vanished exactly while you looked at it -- the derive-from-selection family again. The primitive retains status while selected, as ThreadRow does. Preserved and verified live: Build search filtering with the selected pane retained outside the filtered nodes, both rail toggles (320 -> 0 -> restore), Gang's per-thread provider chips (Pi/Codex bind per selection), status priority, and the nested guide. The migrated-source guard now covers Build and Gang; Spawn's legacy CSS stays intentionally live for step 4. Gates: build, typecheck 0, 93 tests (was 89). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…pied link sol's step 4 -- the step it flagged highest-risk -- interrupted mid-flight by a task kill and finished anyway; verified here before landing. The delegate's own reviewer blocked on "three hrefs, one rendered target", but it reviewed the mid-flight tree: the finished work routes every cause row through /?spawn=<id>#spawn-<id> with validateSearch on the landing route, so a copied link or no-JS navigation SSRs with the matching pane target present. Verified: /?spawn=cli renders id="spawn-cli" server-side. Verified live: three openable 28px anchors, seven scenery rows that are inert divs (no href, default cursor, untabbable), the rail toggle, zero legacy sub-row/gang-row markup left in the demo, and A/B/A leaving read threads read. Prerender carries every title and final source (checked with python -- grep silently fails on this file's 52k-char lines). Arrival feeds only attentionRevision now; the local read Set and the row entrance animation are gone. New spawn-demo-routing module owns the search-param contract with its own tests. Gates: build, typecheck 0, 98 tests (was 93). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Elevation pass on the marketing site (
apps/web), built on the fork for review before anything is proposed upstream.The starting point was a page that described bb in prose and illustrated it with generic marketing furniture. The goal was a page that shows the product: every demo is either a real recording of a seeded workspace or a component rebuilt from the app's own tokens, and nothing on the page claims a capability bb does not have.
What changed
Sections and rhythm. Full-bleed alternating white/slate bands with the inner panels retained, no hairline dividers, no eyebrow captions. Header is non-sticky, matching production. The release callout is a pill.
Seven demo components, hand-built from the app. Tasks board, working-tree review, subagents, ask-a-question, plugin build, the multi-agent gang, and background spawns. Each one is a real bb window — the same chrome, rails, row metrics, glyph vocabulary, and state tints the shipped app uses. Several were verified field-by-field against live captures of a seeded instance rather than from memory.
Rendering law. Every demo is fully legible in prerender, with JS off, under
prefers-reduced-motion, and in a single screenshot. Motion enhances; it never owns whether content exists. Loops reserve their own space so no section changes height as it plays.Honesty law. Demo content comes from a seeded storefront workspace, never real user data. Stats (stars, forks, contributors, merged PRs) are fetched at build time by
apps/web/scripts/refresh-github-stats.mjsand baked. The merged-PR feed is real merges.Copy. Rewritten toward concrete mechanism over abstraction, with the em-dash habit removed — it was the page's strongest tell that a model wrote it.
Primitives. Button family reworked (no y-axis lift on hover), a copy control that morphs icon to checkmark with no tooltip, sanctioned type tokens, and theme colors derived from the
--canvas/--inkanchors rather than hardcoded literals.Verification
pnpm exec turbo run typecheck test build --filter=@bb/web— clean, 73 tests passing. Demos verified visually at desktop and mobile widths in a headed browser.What this PR is for
Review. It is open in the fork so it can be read publicly and critiqued in detail. It is not proposed upstream yet.