Skip to content

Landing page elevation: real bb surfaces, section rhythm, and a rebuilt demo set - #6

Open
lnittman wants to merge 308 commits into
mainfrom
bb/landing-elevation
Open

Landing page elevation: real bb surfaces, section rhythm, and a rebuilt demo set#6
lnittman wants to merge 308 commits into
mainfrom
bb/landing-elevation

Conversation

@lnittman

Copy link
Copy Markdown
Owner

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.mjs and 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/--ink anchors 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.

AGENT GENERATED: by Claude Fable 5

ymichael and others added 22 commits August 19, 2026 22:43
## 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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +1131 to +1133
.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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

lnittman and others added 7 commits August 20, 2026 10:17
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.
lnittman and others added 30 commits August 23, 2026 10:33
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.