Skip to content

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

Open
lnittman wants to merge 276 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 276 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

lnittman and others added 30 commits August 19, 2026 20:22
- Every merged-PR row is now a real link to its pull request, with the
  PR number and date in the meta and a quiet hover ground; the feed
  data carries numbers and URLs from the refresh pipeline.
- The Ask card finishes its story: submit presses and the card rests
  on 'Answered — Single code per cart'; the subagent fragment gains its
  report-back line under the completed child.
- The bento's media windows nudge on card hover; the footnote row
  under the grid is removed; the stat cards pick up a hairline top
  light and label tracking.
- The closer's bb mark drops its background tile (the nav mark was
  already plain).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited against the design-engineering checklist and the details list:
grayscale moz smoothing joins antialiased at the root; body paragraphs
pick up text-wrap: pretty (titles keep balance); the hero enter gains
the blur signature (y + blur + opacity); buttons get a real press
scale at the press duration; the elevated media frames and the spawn
window fold their hairlines into the shadow ring (shadows over borders
for elevation); feed avatars carry the 10% neutral inset outline; and
pointer-only hover polish is gated behind hover/pointer media queries.
Already compliant and verified: no transition:all anywhere, tabular
numerals on every changing number, interruptible transitions for
interactions with keyframes reserved for one-shot storyboards, and
reduced-motion rests for every animated surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce; crisp feed card

- The spawn demo becomes a looping two-source story: the CLI types and
  its thread arrives; then the source crossfades to a Telegram message
  from Hermes and a second thread arrives — 'anything can kick off
  work', shown twice over. Loops while on screen, rests fully settled
  offscreen and under reduced motion. (Also excised a duplicated
  component suite my earlier region-splice left behind.)
- The bento sheds its essay voice: 'More than a chat window.' over
  'bb carries the work around the conversation — building, reviewing,
  delegating, deciding.', with card bodies tightened to match.
- The merged-PR feed's fade now lives on the inner list only — the
  card's own edges stay crisp — and the card grows to eight visible
  rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hero theater and the showroom drop their white room containers;
both windows now rest straight on the band with their shadow rings at
full stage width. One frame per object, no double-framing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'Anything can kick off work', fully shown: three causes stacked on the
left — the CLI command, a Telegram message from Hermes, and a nightly
automation — take the stage one at a time (rest at reduced presence,
live at full), each firing a thread into the big sidebar on the right:
row expands in, title morphs, settles with its unread dot. By the end
of the loop all three threads sit in the sidebar; offscreen and under
reduced motion the machine rests in exactly that state. No room
container — both planes sit directly on the slate. Also excised a
duplicated spawn CSS region and the stale term-plane responsive rules
(same splice-duplication disease as the JSX, now clean in both files).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy (council lane, Stripe's bold-lead grammar as the reference):
- Nine prose em-dashes are gone; the page now carries one, in a UI
  status label where the mark is native. Every section lead had used
  the same spaced-dash apposition, and the metronome was the tell.
- Leads rewritten to claim-then-mechanism: the hero opens on 'Mission
  control for coding agents', the billing fact is promoted to its own
  claim ('No new subscription.'), the Extensions sentence becomes a
  bold secondary claim, and the gang lead drops its participle tail.
  Roadmap voice ('laying the groundwork') leaves the meta description.
- THE missing proof, and it was in the repo all along: agent-created
  PRs must carry an AGENT GENERATED signature, so the stats script now
  counts them. 253 of the 567 PRs merged into bb last month were
  written by agents running in bb. The page finally proves its H1.

Craft:
- New button primitives. Buttons no longer drift upward on hover
  (controls that move under the cursor read as decoration): hover
  deepens tone and elevation, press scales at the press duration,
  focus-visible gets a real ring, and the outline variant carries its
  border in the shadow ring so it reads the same on white and slate.
- The PR feed gains an initial-chip fallback so a missing face can
  never collapse a row, and its hover ground deepens the card tone
  instead of flashing white.
- Mobile: real controls (nav, footer, release rail, buttons, logo
  rail) extend to 44px targets on coarse pointers and small screens
  while the app mock keeps its true density; the spawn machine's grid
  children can shrink again, killing a 32px overflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator direction: the crafted demos (the subagent and Ask cards, the
spawn machine) are the model for how all demo content is presented, and
the recordings were the weak link — desktop frames shrunk until their
text read as decoration. Four more crafted components replace them, all
in the app's own tokens with storyboard constants, looping while on
screen and resting settled offscreen and under reduced motion:

- GangDemo: six threads working across three projects with provider
  glyphs and live statuses, one open beside its accumulating transcript,
  a child nesting under its parent as it completes.
- BuildDemo: a prompt types itself, and the three surfaces it produced
  land one after another — the panel, the bb review command, the skill.
- TasksBoardDemo: the storefront board dealing its cards column by
  column, one ticking over to done.
- ReviewDemo: the promo-engine diff landing line by line (the real
  prototype-key fix), then the working tree arriving and Commit arming.

Every recording, poster, and crop retires with the DemoWindow component
and its CSS; the page now composes entirely from crafted product
surfaces, which also makes it responsive by construction rather than by
zoom-cropping desktop canvases.

Also: the stat roll can no longer hold a zero — it reveals after a hold
regardless of scroll, since a zeroed numeral is a false statement under
the page's rendering law.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A macOS download cannot be completed on the phone the reader is
holding. On small screens the copy-command becomes the first and
stronger-ringed action (it can be copied, sent, or run against a
remote machine) and the download demotes to an outline below it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The copy affordance follows the keris pattern: the copy glyph and a
check share one grid cell and cross-fade through blur and scale with no
vertical drift (a y-shift reads as the icon falling), so the control
never reflows and the confirmation lands where the eye already is. The
'Copied to clipboard' toast is gone with it. Both glyphs are drawn at
the mock's stroke weight rather than pulled from an icon set, so they
swap cleanly.

The PR feed's row tiles become real objects: inset 22px from the card
wall instead of running nearly edge to edge, rounded 12px for their
height, and padded 14px inside so the avatar and title never sit
against the tile edge. The hover ground is now the page's own white
with a hairline ring rather than a brighter flash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The feed card's wall padding moves onto the scrolling clip, so a row's
1px ring has 22px of room on both sides instead of being shaved off by
the clip box (the masked-border edge).

The crafted demos drop from the 1200px editorial rail to a 1040px demo
measure and lose their stretched empties, so they read as objects with
air around them rather than walls of product.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bento demos were reading as broken because each one ended early
inside a fixed frame, leaving a half-empty white box under it. Dia's
device fixes the cause: the demo content runs PAST the bottom of its
frame and dissolves there, so the crop says 'there is more of this'
instead of 'this ran out'. The mask lives on the content only — the
frame stays a crisp object, exactly as specified.

Every demo now carries enough material to reach the edge: the board
deals four cards per column, the diff runs from the promo-engine fix
into the test it added, the subagent tree continues past its
siblings, and the Ask thread keeps working after it is answered. The
working-tree bar moves above the hunk so a real control never
dissolves — only content does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The command control becomes two cells instead of a padded row: the
command, and a copy well. The button drops its own padding so the
divider meets the top and bottom edges instead of floating at text
height, each cell carries its own padding, and the glyph sits
optically centered in the well (15px of interior on both sides of the
1px rule). The well also picks up its own hover ground, so the copy
target reads as a target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The demos really were broken, and the cause was a mangled selector: an
earlier regex purge left a dangling `.bento-window` token in front of
`.bento-component`, turning it into a descendant selector that never
matched. All four bento demos silently lost their interior padding and
flex layout, so content sat flush against the frames, diff rows ran
edge to edge, and the +3/-1 counts clipped mid-glyph.

With the rule repaired (padding open at the bottom so content bleeds
into the dissolve), each demo was raised to the reference bar:

- The dissolve becomes a three-stop eased mask instead of a two-stop
  ramp, so it reads as atmosphere rather than a wipe.
- Every demo fills its frame: the Ask card carries its follow-up work,
  the build card gains its Registered/Wrote-the-skill beats and loses
  ~120px of dead space, the gang thread runs a six-beat transcript
  ending with the open Codex thread completing on screen.
- The spawn machine gets equal columns (the full command finally types
  without clipping), a top-aligned sidebar, and cause tiles that rest
  crisp instead of permanently dimmed — a rendering-law fix, since a
  demo's resting state must be its true state.
- Radii unify under a new --r-field token; card padding and type settle
  at 20px/13.5px across all seven demos; every loop's rest beat is
  longer, so the demos spend most of their time settled.

Paired with Claude Fable 5, which diagnosed the selector and did the
craft pass; verified here at 1440 and 390 with typecheck and tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ights

Completes the paired fidelity pass (the worker hit its session limit
mid-edit, leaving components whose CSS was never written).

Fidelity — the demos now carry what the real surfaces carry:
- The board gains the Tasks header, project dot, List/Board segmented
  control, refresh, per-column status glyphs and counts and +, card
  priority glyphs, and the right rail with All tasks / Active / Projects.
- The diff gains its All changes header, file counts, line numbers, the
  unmodified-lines collapse, and a second file below.
- Subagents becomes the app's two-pane shape: thread rail beside the
  open thread, unread dots on quiet rows.
- The gang thread gains the real footer stack: working-tree bar,
  follow-up composer, and the context row (model, worktree, branch).
- Ask gains its waiting banner and numbered option hints.

Fixes found while finishing it:
- No invented OS chrome anywhere. All five traffic-light clusters are
  gone, including the hero mock's, and the spawn demo's bare window
  strip with it. Real product headers (thread title with Commit, the
  Extensions page header) stay — those are the product, not costume.
- Masking an element also clips whatever it paints outside its border
  box, so the frames' outset rings were losing their left and right
  edges to the dissolve. The ring is inset now and survives.
- Arriving rows reserve their space instead of collapsing height, so
  the spawn section no longer grows 97px and the gang 21px on every
  loop. Both are height-stable now.
- Repaired two selector lists my earlier dot-removal regex cut through.
- Bento subtitles hold one line; the first card's copy stops
  under-selling its own demo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
)

## What was wrong

The thread right panel could show only one active tab at a time, so
comparing or retaining multiple panel contexts required repeated tab
switching. Its existing full-screen control had no way to position the
active tab in a sidebar split. This is the foundation for get-bb#1841.

## What changed

- Added a persisted right-panel split layout that targets the active tab
and reuses the existing pane arrangement model and controls.
- Kept the existing right-panel header, tab behaviors, outer width
resizer, collapse control, and full-screen behavior.
- Added pane-local content and browser focus/lifecycle handling so
visible split tabs retain the same panel semantics as the unsplit view.
- Added focused coverage for split creation, positioning, resizing, tab
movement, persistence, browser focus, and panel lifecycle.

## How you verified

- `pnpm exec vitest run
apps/app/src/components/secondary-panel/BrowserTabDeck.browser-view-ordering.test.tsx
apps/app/src/components/secondary-panel/ThreadSecondaryPanel.collapseControl.test.tsx
apps/app/src/components/thread/terminal/useThreadTerminalController.test.ts
apps/app/src/views/thread-detail/ThreadDetailSecondaryContent.test.tsx
apps/app/src/views/thread-detail/SplitThreadArea.test.tsx` — 66 tests
passed.
- `pnpm exec turbo run typecheck --filter=@bb/app` — passed.
- `git diff --check origin/main...HEAD` — passed.
- Exact-head visual QA used the same deterministic `Sidebar Split QA`
fixture in the launched bb desktop dev app. Captures are clipped to the
rendered viewport so no off-viewport renderer content is included. Each
caption records viewport and output dimensions.

### Before — `origin/main` `5f4f3dec733612fcf23defd8ac17c22462715e9d`

Viewport 1440 × 900 at DPR 2; image 2880 × 1800.

![Before: unsplit right sidebar on origin
main](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/foundation-before-main-5f4f3dec7-1440x900-2x.png)

### After — foundation `3ad3d0f6c36b0b9b581360d0ca56d1c342b2b1a3`

Viewport 1440 × 900 at DPR 2; image 2880 × 1800. Browser and Side chat
occupy separate sidebar panes under the existing header.

![After: Browser and Side chat split side by
side](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/foundation-after-3ad3d0f6c-1440x900-2x.png)

## Exact-head screenshot matrix —
`3ad3d0f6c36b0b9b581360d0ca56d1c342b2b1a3`

All captures use the same `Sidebar Split QA` project and `Primary split
QA` / `Secondary split QA` threads in the exact-head desktop dev app.

### Page-split configurations

#### Wide — side-by-side thread pages

Viewport 1630 × 900 at DPR 2; image 3260 × 1800.

![Page split with primary and secondary threads side by
side](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/page-wide-3ad3d0f6c-1630x900-2x.png)

#### Medium — stacked thread pages

Viewport 1224 × 768 at DPR 2; image 2448 × 1536.

![Page split with primary above
secondary](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/page-medium-3ad3d0f6c-1224x768-2x.png)

#### Compact — stacked thread pages

Viewport 768 × 900 at DPR 2; image 1536 × 1800.

![Compact page split with primary above
secondary](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/page-compact-3ad3d0f6c-768x900-2x.png)

### Tab-split configurations

#### Wide — Browser and Side chat side by side

Viewport 1630 × 900 at DPR 2; image 3260 × 1800.

![Wide right-sidebar tab split with Browser and Side
chat](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/tabs-wide-3ad3d0f6c-1630x900-2x.png)

#### Medium — Browser and Side chat side by side

Viewport 1224 × 768 at DPR 2; image 2448 × 1536.

![Medium right-sidebar tab split with Browser and Side
chat](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/tabs-medium-3ad3d0f6c-1224x768-2x.png)

#### Compact — full-screen right panel

Viewport 1024 × 900 at DPR 2; image 2048 × 1800. Uses the existing Full
Screen control so both split-tab labels and bodies remain legible under
compact width pressure.

![Compact full-screen right panel with Browser and Side chat split
panes](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/tabs-compact-fullscreen-3ad3d0f6c-1024x900-2x.png)

#### Ultra-wide — Browser and Side chat side by side

Viewport 3440 × 1440 at DPR 1; image 3440 × 1440.

![Ultra-wide right-sidebar tab
split](https://raw.githubusercontent.com/get-bb/bb/8d3314daca1b3f6fdf91dfa960aa6ba1f0c74e59/.github/pr-evidence/1841/legible-final-head-5f4/tabs-very-large-3ad3d0f6c-3440x1440-1x.png)

Fixes get-bb#1841

BB-Thread-ID: thr_4rr623umv4

> AGENT GENERATED: by GPT-5
First step of the interactivity pass. The Ask card stops performing and
starts waiting, which is what the product actually does: its resting
state IS the open question, so prerender, no-JS and reduced motion all
show a real complete card instead of a frame of a script.

The options are real buttons now. Pick one and Submit arms; submit and
the agent proceeds with YOUR answer — each option carries its own
outcome, so choosing 'Allow stacking' gets precedence rules and a
discount floor, not the copy written for a different choice. Keyboard
focus rings and pointer-gated hover included.

Loops elsewhere gain takeOver(): once a visitor drives a demo, its
script stops for good rather than overwriting them a second later.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What was wrong

Dragging the left sidebar in a long thread was slow (DevTools profile:
2.2 s of style recalculation, ~50 ms per frame, vs 78 ms of layout).
Three things in `AppLayout` each forced a restyle of the whole document:

1. Per frame: the live width was written as `--sidebar-width` on the app
root (`SidebarProvider`). A custom property is inherited, so a change on
the root invalidates every descendant, including the timeline. Cost
scales with thread size (~20–45 µs per element).
2. At mousedown/mouseup: `body.style.userSelect` and `body.style.cursor`
(both inherited) → two more full restyles each.
3. At mousedown/mouseup: `IframeDragGuardOverlay` mounted as a sibling
*before* the app root. Inserting/removing a node invalidates every
following sibling's subtree → one more full restyle each.

Items 1–2 date from the initial sidebar implementation; item 3 came with
the iframe guard (get-bb#57). Nothing regressed recently; it only becomes
visible in large threads.

## What changed

- `ui/sidebar.tsx`: new `SidebarWidthContext` and a `width` prop on
`SidebarProvider`. `Sidebar` writes `--sidebar-width` directly on the
two elements that read it (gap and panel), not on the wrapper.
- `ui/theme.css`: `@property --sidebar-width { inherits: false }`, so a
change restyles only those two elements.
- `layout/AppLayout.tsx`: the live width flows through a non-persisted
`sidebarLiveWidthAtom`; only the bridge and `Sidebar` re-render per
frame (`flushSync` keeps the browser-view bounds sync measuring the
updated DOM). Removed body `cursor`/`user-select`: mousedown
`preventDefault` already blocks selection, and the overlay now carries
the resize cursor. The overlay mounts after the app root.
- `lib/iframe-drag-guard.tsx`: required `cursor` prop, placement note.
`ThreadSecondaryPanel` moves its overlay after the panel content for the
same reason.

## How you verified

Repro: dev app with `pnpm seed:perf -- --projects 2 --threads 20
--events 60000`, open the 9k-event thread in headless Chromium, scroll
to the top until ~20k DOM elements are mounted, run a 60-step scripted
drag on the resize handle under a CDP trace.

| | Style recalcs | Recalc total | Max per recalc | Elements/recalc |
Layout total |
|---|---|---|---|---|---|
| Before | 50 | 19,444 ms | 419 ms | 19,423 | 1,087 ms |
| After | 2 | 5 ms | 2.9 ms | 31 | 199 ms |

Manual checks in the browser: width tracks the drag, commits to
localStorage and survives reload, collapse/expand still works, no text
selection during drag, overlay shows `col-resize`.

Tests (fail before, pass after): `AppLayout.sidebar-resize.test.tsx`
(drag writes only gap/panel, never root or body; overlay mounts after
the root with the cursor), `sidebar.test.tsx` (width lands on gap/panel,
not the wrapper), `theme.test.ts` (`@property` non-inherited). `pnpm
exec turbo run typecheck lint --filter=@bb/app` passes; 110 test files /
888 tests pass in the touched areas.

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Stack layer 1 of 4 for the bb mobile app (stack #1990). No prerequisite;
the next layer is get-bb#1987.

## What was wrong

The native mobile app (`apps/mobile`, layer 3) needs the web app's
platform-neutral client logic — sidebar grouping and sorting, thread
read state, prompt draft and submission policy, mention triggers,
timeline merge/paging helpers, renderable-patch rules, the terminal
WebSocket transport, panel tab schemas, file preview, localhost link
rewriting, route builders. Today these live inside `apps/app/src` next
to DOM and react-router code, so a React Native app cannot import them.

## What changed

- New `packages/client-core` (`@bb/client-core`): the pure modules moved
out of `apps/app` unchanged, plus a `test/no-dom.test.ts` guard that
walks `src/**` and fails on
`window`/`document`/`localStorage`/`navigator`/react-router/`@/`
references.
- `apps/app`: every old path is a one-line re-export shim, so web
imports and tests keep working; the few files that mixed DOM and pure
code (`route-paths.ts`, `fixed-panel-tabs-state.ts`,
`useThreadTimelineController.ts`,
`localhost-link-rewrite-preference.ts`, `terminal-websocket-url.ts`,
`TimelineFileDiffBlock.tsx`, `secondaryPanelTabState.ts`) keep the
web-only parts and import the rest from client-core. 19 tests moved with
their modules.
- `TerminalWebSocketTransport` treats an undefined
`socket.bufferedAmount` as 0 (React Native's WebSocket never sets it),
with a regression test.
- `@bb/client-core` depends on `@bb/domain`, `@bb/server-contract`,
`@bb/core-ui`, `@bb/thread-view`, `@bb/fuzzy-match`,
`@bb/desktop-contract` (pure), `zod`. No React.

No behavior change in the web app is intended.

## How you verified

- `pnpm exec turbo run typecheck lint test --filter=@bb/client-core
--filter=@bb/app` — client-core 236 tests, app 2955 tests / 392 files,
lint 0 errors (the 144 warnings pre-exist on main).
- `packages/client-core/test/no-dom.test.ts` guards the DOM-free rule;
the transport test fails without the `bufferedAmount` fix.

Part of the bb mobile app plan (`plans/bb-mobile-expo.md`, lands in
get-bb#1988).

> AGENT GENERATED: by Claude Opus 5

---------

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

Stack layer 2 of 4 for the bb mobile app (stack #1990). Prerequisite:
get-bb#1986 (`@bb/client-core`). Next layer: get-bb#1988 (the app).

## What was wrong

A native client needs a few small, additive pieces the server, cloud,
SDK, and test harness do not have today: a way to tell the server it is
a mobile client, SDK types that survive React Native's global
`Response`/`FormData` declarations, a shared contract for the two
plugin-rendered pending interactions so the phone can render them
natively, a deterministic approval path in the fake provider for e2e, a
harness that can run on a fixed port behind a stub gate, a way to mint a
connect pairing code for a phone, and app-link association files so
universal links can work.

## What changed

- `packages/config` + `apps/server`: request-side `RequestAppSurface`
(`desktop | web | mobile`) for `x-bb-app-surface` + telemetry; the
server config union stays `desktop | web`. Test added.
- `packages/sdk`: `SdkResponseLike` structural constraint in
`response.ts`/`transport.ts` (Hono client responses stay assignable
under RN globals); `projects.sidebarBootstrap()`; public-types test
updates.
- New `packages/plugin-interaction-contracts`: the ask-user-question and
secret-request payload/resolution schemas moved from the two plugins'
private `contracts.ts`; both plugins re-export them (their tests
unchanged).
- `packages/agent-runtime` fake provider: `approve:<kind>` control token
(`command | file_change | permission_grant | plan`) emits an approval
interactive request; adapter decodes/encodes approval payloads; 5 tests.
- `tests/integration`: harness `serverPort`/`bindHost` options;
`mobile-e2e/backend.ts` (seeded long-lived harness) and
`connect-stub.ts` (TLS stub gate in front of the harness) + scripts +
turbo tasks.
- `plugins/connect`: "Add mobile device" QR/code in Settings → Remote
access and `bb connect machine-code [--json]` (uses the existing
`createMachineCode` RPC; 409 machine-limit surfaced). Guide template,
bb-cli SKILL, `docs/multiple-devices.md`, `docs/configuration.md`
pairing section updated per `docs/cli-guide-and-skill.md`.
- **Hidden behind the new `mobileApp` experiment** (off by default;
Settings → Experiments → "Mobile app", or `bb settings experiment
mobileApp true`) until the app is generally available. `@bb/domain`
gains the key; the connect plugin reads it through its loopback SDK
(`bb.sdk.system.config()`) on every call — a new `mobilePairing` rpc
tells the panel whether to render the section, and `createMachineCode` /
`bb connect machine-code` refuse with a pointer to the toggle. No new
plugin-SDK API. With the experiment off, a paired install sees no visual
change from this PR.
- `packages/connect-client`: pairing helpers used by the phone.
- `apps/connect` (gate) + `apps/web` (apex) + `packages/connect-db`:
serve `/.well-known/apple-app-site-association` and
`/.well-known/assetlinks.json` before the session gate (static JSON
only; Android fingerprints from `ASSETLINKS_SHA256_FINGERPRINTS`, empty
if unset). No auth change.

No daemon contract change → `HOST_DAEMON_PROTOCOL_VERSION` untouched.

## How you verified

- `pnpm exec turbo run typecheck lint` for config, server, sdk,
plugin-interaction-contracts, ask-user-question, secrets, connect
plugin, agent-runtime, integration-tests, connect-client, templates,
@bb/connect, @bb/web, connect-db, cli — pass (the only failure is the
pre-existing `conversation-outline-parity.test.ts` typecheck on main
from get-bb#1657).
- Tests: sdk 96, agent-runtime 413, connect plugin 88, templates 43,
connect/connect-db/connect-client, server telemetry + public route tests
(342), `@bb/integration-tests test:smoke` 28 — all pass.

Part of the bb mobile app plan (`plans/bb-mobile-expo.md`, lands in
get-bb#1988).

> AGENT GENERATED: by Claude Opus 5

---------

Co-authored-by: Claude <noreply@anthropic.com>
Second step of the interactivity pass, following the split: state-y
demos rest in a real complete state and respond; sequence stories keep
performing (spawn, gang, build are untouched here).

- The board stops dealing itself its own cards. It rests as a finished
  board, and its List/Board control is now a real segmented control
  that switches to the app's list rendering — status glyph, id,
  title, priority — with cards and rows lifting under the pointer.
  A task board that animates on its own is a screensaver, not a product.
- Every row in the subagent rail opens its thread, as it does in the
  app. The pane used to hold three skeleton bars; it now shows the
  selected thread's real transcript, so the visitor can walk the whole
  spawn: the parent that spawned a subagent, the child that found the
  prototype-key and empty-cart gaps, and the report coming back.

No hints, no labels: the controls read as controls, with pointer-gated
hover and real focus rings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gang's working-tree bar is a nowrap row of metadata; at 390 it was
pushing the page 18px sideways, so it wraps now. The subagent demo
stacks instead of splitting a phone into a 148px rail beside a 92px
pane, and its mobile override is placed after the base rule so source
order actually lets it win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## What was wrong

The \`claudeCodeMockCliTraffic\` experiment started a loopback HTTP
proxy per Claude Code session and rewrote the user-agent and billing
headers of the SDK's API traffic. The experiment is a dev-only tool, it
is off by default, and it has no product use. It still cost a field on
the shared \`ProviderExecutionContext\`, a wire field on runtime
options, a Settings toggle, and a daemon stderr hook.

## What changed

- \`packages/domain\`: removed the \`claudeCodeMockCliTraffic\`
experiment key, the endpoint/config schemas, and the default config.
Deleted \`claude-code-mock-cli-traffic.test.ts\`.
- \`packages/agent-runtime\`: removed the field from
\`ProviderExecutionContext\`, execution option comparison, and context
construction.
- \`apps/server\`: \`thread-commands.ts\` no longer resolves or sends
the option.
- \`plugins/provider-claude-code\`: deleted
\`mock-cli-traffic-proxy.ts\` and its test. The bridge builds the
session env directly; \`prepareSessionEnv\`, the proxy lifecycle on the
thread session, and the \`NO_PROXY\` loopback helper are gone. Session
params and bridge commands no longer accept the field.
- \`packages/plugin-sdk\`: removed the re-exports.
- \`apps/app\`: removed the Settings → Experiments toggle and the
default in \`system-config-atoms\`.
- \`apps/host-daemon\`: removed the stderr hook that logged proxy
requests.
- \`packages/host-daemon-contract\`: **bumped
\`HOST_DAEMON_PROTOCOL_VERSION\` to 137** because the runtime options
schema lost a field.
- Docs: updated \`docs/api_to_audit.md\` and
\`plans/agent-provider-plugin-surface.md\`.

Stored \`claudeCodeMockCliTraffic\` rows in \`system_experiments\` are
ignored by the key/value reader, so no migration is needed.

## How you verified

- \`pnpm exec turbo run typecheck\` for domain, agent-runtime, server,
host-daemon, app, plugin-sdk, provider-claude-code, db,
host-daemon-contract, desktop: pass.
- \`pnpm exec turbo run test\` for domain, agent-runtime, db,
host-daemon-contract, plugin-sdk, provider-claude-code, host-daemon,
app, desktop: pass.
- \`@bb/server\` tests: all pass except
\`internal-skill-trees.test.ts\`, which fails on file mode 0664 vs 0644
from the local umask and is unrelated.

Fixes #

> AGENT GENERATED: by Claude Opus 5

---------

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

The parent-tool-call backfill added in get-bb#1976 evaluated `json_extract`
for any historic event payload containing the parent key text, so
malformed stored JSON aborted migration 0103 and prevented server
startup. The byte-budget optimization from the same PR also aggregated
every matching payload in a client-selected turn-details range before
its early-stopping iterator ran, allowing a very wide range to add one
or two unbounded synchronous scans.

## What changed

- Guard migration 0103 with `json_valid(data)` so malformed historic
payloads remain unbackfilled instead of aborting the upgrade.
- Accept the original merged 0103 hash so development databases that
already applied it continue to pass migration-history validation.
- Limit the scalar byte-total preflight to 2,001 matching events, which
keeps the aggregate fast path for normal event-budgeted timeline windows
and falls back to the existing exact, early-stopping iterator for
unusually long ranges.
- Add regressions for malformed matching JSON, the historical migration
hash, and a wide byte range whose older payload must not be visited by
the aggregate.

This is a database/server implementation fix only. There is no
server/host-daemon wire change, so `HOST_DAEMON_PROTOCOL_VERSION` is
unchanged.

## How you verified

- `pnpm exec turbo run typecheck --filter=@bb/db --force`
- `pnpm exec turbo run test --filter=@bb/db --force` — 29 files, 406
tests passed
- `pnpm exec prettier --check packages/db/src/data/events.ts
packages/db/src/migration-history.ts
packages/db/test/data/events.test.ts packages/db/test/migrate.test.ts`
- `git diff --check`

Follow-up to get-bb#1976 and its [migration
finding](get-bb#1976 (comment))
and [range-scan
finding](get-bb#1976 (comment)).

> AGENT GENERATED: by GPT-5
Stack layer 3 of 4 for the bb mobile app (stack #1990). Prerequisites:
get-bb#1986 (`@bb/client-core`) and get-bb#1987 (core prep). Next layer: get-bb#1989 (push
notifications).

## What was wrong

bb has no native phone client. The PWA works on a phone but is bounded
by mobile-web limits (keyboard/viewport hacks, no push, no background
reconnect, no secure credential storage, no camera/photo attachments,
WebKit style-recalc stalls). Plan, decisions, and research:
`plans/bb-mobile-expo.md`, `plans/bb-mobile-research/`; execution log
with per-phase verification: `plans/bb-mobile-progress.md`.

## What changed

New `apps/mobile` (`@bb/mobile`, Expo SDK 57 / React Native 0.86 / Expo
Router / NativeWind v5, iOS first; ~81k lines of app code, ~20k of unit
tests, 26 Maestro flows). Everything else in this PR is toolchain/docs;
no server or web behavior changes.

- Connectivity: server profiles in SecureStore; Direct mode (LAN /
Tailscale / simulator) and bb connect (machine enrollment by QR/code →
desktop-session cookie in the native jar, renewal, re-pair); per-profile
SDK + realtime manager (ping/pong, watermark reconnect, AppState
suspend/resume) + QueryClient with realtime invalidation; global
mutation error toasts.
- Screens: drawer sidebar + home thread list (organize/sort, pinned,
unread glyphs, long-press actions), search, archived, compose with
pickers (project, provider, model+reasoning, permission mode, service
tier, environment incl. reuse/worktree/branch/path, machine), new
project + project settings + machine setup, thread detail (FlashList
timeline with every row kind via a renderer registry, native markdown,
native diffs, ANSI, lightbox, unread divider, ToC, prompt-stack cards),
prompt area (composer with mentions/typeahead/attachments/voice/drafts,
send/queue/steer/stop, queued messages, pending-interaction banner incl.
approvals, questions, ask-user-question and secret-request forms,
context banner with git/PR/parent/children, message/thread/git action
sheets, fork/handoff), workspace panel (Info, Diff, Files + previews,
terminal = bundled xterm WebView with RN-owned socket + accessory bar,
synced thread tabs), settings (general, appearance palettes from
`theme.css`, experiments, providers, usage, machines list/detail/add,
updates, plugins management + marketplaces, skills library/registry,
haptics), deep links (`bb://`, universal link mapping, thread-open
signal), outbound share.
- Design system: generated native theme tokens
(`scripts/generate-native-theme.ts` from `theme.css`, drift-tested),
primitives (sheets, action sheets, toasts, etc.), Inter + Fira Code,
hugeicons.
- Toolchain: Metro resolves `@bb/*` from TS source (`source` condition
scoped to workspace packages), `pnpm patch` for
`expo-modules-jsi@57.0.4` on Xcode 26.2, scoped `lightningcss` override
for NativeWind v5, turbo/eslint/prettier wiring,
`.github/workflows/mobile-e2e.yml` (label `mobile-e2e` / nightly;
Release build + Maestro) and `mobile-runner-probe.yml`, `eas.json`
profiles, docs (`docs/platform-support.md`,
`docs/repository-overview.md`).
- Early access: the bb-side pairing surfaces (Settings → Remote access →
Add mobile device, `bb connect machine-code`) are behind the `mobileApp`
experiment from get-bb#1987; the app's own Settings → Experiments screen lists
that toggle too (it is shared server state).
- Not in this layer: push notifications (get-bb#1989). Not yet: Android,
plugin frontends (SPA-in-WebView planned), inbound share (needs a native
rebuild), TestFlight (needs an EAS account).

## How you verified

- `pnpm exec turbo run typecheck lint test --filter=@bb/mobile` — 794
tests / 117 files, 0 lint findings.
- Every phase was built by parallel agents and independently
re-verified; the full Maestro regression (26 flows) passed on the iPhone
17 Pro simulator (iOS 26.3) against the seeded harness backend
(`tests/integration/mobile-e2e/backend.ts`) and the connect stub gate;
results table and screenshots list in `plans/bb-mobile-progress.md`.
Connect enrollment was also exercised against the real `bee.getbb.app`.
- On this layer specifically (no push): `phase1-shell`, `phase4b-send`,
`phase5-links` flows re-run and pass.
- Known gap carried in the log: on bb connect profiles the left-edge
drawer swipe can also press the home row under the touch (Direct
profiles are fine).

> AGENT GENERATED: by Claude Opus 5

---------

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

The full validation pass exposed three independent boundary and fixture
defects: esbuild metafile inputs and Tailwind scanner dependencies could
use different canonical path spellings on macOS or through symlinked
plugin roots, causing opted-in dependency CSS to be omitted; `fs.watch`
events without a filename were silently dropped, so a builtin source
edit could miss hot reload; and the host-upgrade compatibility
regression fixture loaded every real bundled plugin and timed out under
full-suite contention.

## What changed

- Canonicalize every bundled esbuild input with `realpath` before
intersecting it with Tailwind dependency scans, with a symlinked-root
regression fixture.
- Treat a filename-less builtin source watch event as an unknown change
below the plugin root so the normal debounced reload path still runs.
- Persist the minimal installed-plugin row needed by the host-upgrade
compatibility test and disable unrelated bundled plugins in that
fixture.
- No host-daemon wire contract, CLI surface, guide, or configuration
behavior changed.

## How you verified

- `pnpm lint` — passed with 0 errors (144 existing warnings).
- `pnpm build` — 12/12 Turbo tasks passed.
- `pnpm typecheck` — 69/69 Turbo tasks passed.
- `pnpm test` — 65/65 Turbo tasks passed, including 25 fake integration
files / 55 tests.
- `pnpm test:integration` — 64 agent-runtime real-provider tests and 11
server/daemon E2E tests passed across Codex, Claude Code, and Pi.
- Forced `@bb/server` stress run — 191 files / 1,786 tests passed
together.
- Completed `qa/manual-runbook.md` against an isolated standalone server
and daemon: attachments, parent/child threads, shared and managed
environments, archive lifecycle, daemon restart/offline/hot-replace
recovery, all provider passes, and approve/deny/grant interactions
passed. SQLite integrity returned `ok`; the final log audit found no
unexpected fatal or protocol-mismatch signatures.
- After rebasing onto current `main`, forced typechecks for `@bb/server`
and `@bb/plugin-build`, the complete `@bb/plugin-build` test suite, and
both affected server test files all passed.

Fixes get-bb#1915 (regression hardening; the issue was already closed)

> AGENT GENERATED: by GPT-5 Codex
…obile (get-bb#1995)

## What was wrong

get-bb#1993 removed the `claudeCodeMockCliTraffic` experiment key from
`@bb/domain` while get-bb#1988 (the mobile app) was in flight. After both
merged, `main` fails `@bb/mobile` typecheck:
`apps/mobile/src/screens/settings/ExperimentsSettingsScreen.tsx` still
lists the key, and `apps/mobile/e2e/scripts/phase7-settings-reset.js`
still PUTs it to `/api/v1/settings/experiments` (the experiments record
is exhaustive under zod 4, so the reset would 400 and the nightly Mobile
E2E would fail).

## What changed

- Removed the row from the mobile Experiments screen.
- Removed the key from the e2e settings-reset payload.

## How you verified

- `pnpm exec turbo run typecheck lint test --filter=@bb/mobile` on
`main` + this change: all green (119 test files). Typecheck fails on
`main` without it.
- `git grep claudeCodeMockCliTraffic` leaves only historical
migration/test references.

> AGENT GENERATED: by Claude Opus 5

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

Timeline messages and Markdown tables performed synchronous initial
geometry reads from separate `useLayoutEffect` instances, then repeated
those reads when the browser delivered the initial `ResizeObserver`
notifications. On a large rendered timeline, the per-component reads and
writes repeatedly forced layout instead of letting the browser deliver
one geometry batch. The exact-base diagnostic rendered 78 timeline rows,
33 Markdown previews, and 13 wide tables: it observed 80 synchronous
message scroll/client-size reads and 26 synchronous Markdown-root
rectangle reads before observer delivery.

## What changed

- Modern browsers now use the initial `ResizeObserver` delivery for
message overflow and table geometry; synchronous measurement remains
only as the no-`ResizeObserver` fallback.
- Markdown table geometry uses one shared observer. Each callback
deduplicates affected registrations, completes all content/clip geometry
reads, and only then writes `--md-content-w` and
`--md-table-breakout-max`.
- Focused tests model initial observer delivery, assert that nothing
reads geometry before that delivery, cover shared batching, and retain
resize, overflow, clipping, continuation-marker, and cleanup behavior.
- No daemon protocol, CLI, server policy, virtualization, or unrelated
performance behavior changes.

This reconstructs assigned source commit
`1d23f56a2b4fca7f775fc5a82e38c28c9f10a4a8` on current main. The shared
message observer from `5fcb4b3b1` and table clipping from `da1470261`
were already upstream. I retained those implementations and adapted the
table path to batch the newer clipping geometry; the source commit's
per-table observer was therefore not copied literally.

## How you verified

Base/head: `a873435d32dbcf24e28408ae315823c333ed858f` →
`bf85e71daed8ba37791e371d6fe27a033720b865`.

Production fixture command:

```sh
pnpm seed:perf -- --data-dir /tmp/bb-perf-pr4/data --reset --projects 1 --threads 1 --events 1400 --seed 41
```

The resulting thread had 1,406 events and 47 completed agent-message
items. I appended the same six-column wide Markdown table to each
completed agent message, then copied the resulting SQLite fixture for
baseline and after. The common desktop path rendered 78 rows / 33
previews / 13 tables / 2,520 DOM elements; compact mobile rendered 33 /
13 / 5 / 1,248. Headline runs used production `pnpm start` servers,
DevBrowser-managed Chromium, cold disabled cache on every repetition,
100 ms network latency, 524,288 B/s down, 131,072 B/s up, 4× CPU
throttling, one warmup plus seven measured repetitions, and 2,000 ms
post-ready settle. Baseline and after were alternated run-by-run while
both servers were up; odd/even order was reversed to limit drift.
Hardware: Apple M4 Max, 48 GiB RAM, macOS 26.5.1, Node 22.23.1, pnpm
9.15.0.

Median / nearest-rank p95 (with seven samples, p95 is the observed
maximum):

| Metric | Desktop baseline | Desktop after | Median delta | Mobile
baseline | Mobile after | Median delta |
|---|---:|---:|---:|---:|---:|---:|
| Scripting (ms) | 739.1 / 753.9 | 721.0 / 772.0 | -2.4% | 534.8 / 571.6
| 555.9 / 625.9 | +3.9% |
| Layout (ms) | 125.3 / 180.1 | 84.5 / 87.7 | -32.6% | 59.6 / 66.4 |
47.5 / 51.1 | -20.2% |
| Style recalc (ms) | 545.6 / 560.2 | 554.2 / 580.1 | +1.6% | 260.9 /
322.4 | 257.6 / 297.0 | -1.3% |
| Total task time (ms) | 1,662.4 / 1,769.1 | 1,598.4 / 1,703.6 | -3.9% |
1,086.6 / 1,198.2 | 1,078.5 / 1,238.1 | -0.7% |
| Long task count | 9 / 9 | 9 / 10 | 0 | 4 / 4 | 4 / 4 | 0 |
| Long task total (ms) | 1,112 / 1,126 | 1,027 / 1,086 | -7.6% | 364 /
442 | 428 / 460 | +17.6% |
| Max long task (ms) | 308 / 343 | 245 / 257 | -20.5% | 157 / 164 | 160
/ 178 | +1.9% |
| Timeline ready (ms) | 3,555 / 3,575 | 3,492 / 3,563 | -1.8% | 3,393 /
3,419 | 3,381 / 3,418 | -0.4% |
| LCP (ms) | 3,552 / 3,576 | 3,496 / 3,572 | -1.6% | 3,384 / 3,416 |
3,376 / 3,400 | -0.2% |
| Layout count | 46 / 46 | 31 / 32 | -32.6% | 47 / 48 | 42 / 42 | -10.6%
|
| Style count | 253 / 260 | 225 / 257 | -11.1% | 248 / 259 | 239 / 242 |
-3.6% |
| DOM elements | 2,520 / 2,520 | 2,520 / 2,520 | unchanged | 1,248 /
1,248 | 1,248 / 1,248 | unchanged |

The exact diagnostic moved all targeted message/table reads into
observer callbacks and reduced observer delivery from 21 callbacks / 54
entries to 9 / 42. It left the same DOM size. Manual production checks
passed for initial desktop render, resizing 1440→900 px, clipped table
breakout variables, contained mobile horizontal table overflow (358 px
client / 485 px scroll width with no page overflow), and a
streaming-style height update/removal (319.375→2,207.5→319.375 px while
width/limit stayed 532/580 px).

Validation commands:

```sh
pnpm exec turbo run test --filter=@bb/app --force -- src/components/thread/timeline/GeneratedConversationMessage.test.tsx src/components/thread/timeline/conversation-message-overflow.test.tsx src/components/ui/markdown-preview.test.tsx
pnpm exec turbo run typecheck build --filter=@bb/app --force
pnpm exec turbo run test --filter=@bb/app --force
```

Results: focused 3 files / 37 tests passed; typecheck and production
build passed; full app suite 411 files / 3,142 tests passed with 3
skipped.

Caveat: DevBrowser provides Chromium here. The compact/mobile run used
Chromium device metrics and touch emulation; Safari/iOS WebKit was not
profiled, so these results do not extrapolate to Safari.

Fixes: none (standalone performance follow-up).

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

Plugin-build already canonicalized esbuild metafile inputs and
dependency package roots, but Tailwind's scanner can still return a
symlink path for a matched source file while esbuild records the symlink
target. The raw string intersection treated those two spellings of the
same file as different, so a bundled workspace/package-manager
dependency that declared `bb.pluginTailwindContent` could silently lose
utilities used from symlinked source entries.

## What changed

- Canonicalize every Tailwind-matched dependency file with `realpath`
before intersecting it with the already-canonical esbuild input set, and
deduplicate files by filesystem identity.
- Harden the real plugin-build fixture so the plugin consumes a
workspace dependency linked through `node_modules`, that dependency
declares `bb.pluginTailwindContent`, and its bundled source entry is
itself a symlink. The test still verifies that unbundled and explicitly
excluded dependency files do not add utilities.
- No host-daemon wire contract, public plugin API, CLI/configuration
surface, guide, or runtime CSS scoping behavior changed; no protocol
bump or migration is needed.

## How you verified

Regression proof against the exact baseline:

- Baseline: freshly fetched `origin/main` at
`6b7c58fd1bac2bad20c5d6beed3d0c1032824e82`.
- With only the strengthened test applied, `pnpm exec turbo run test
--filter=@bb/plugin-build --force` failed because generated `app.css`
did not contain `.tracking-widest{`.
- With the fix applied, `pnpm exec turbo run typecheck test
--filter=@bb/plugin-build --force` passed all 8 Turbo tasks;
plugin-build reported 7 test files passed, 41 tests passed, and 1
existing toolchain/platform skip.
- `git diff --check` passed.

Isolated before/after profile:

- Device/configuration: Mac16,6 (Apple M4 Max, 48 GiB), arm64, macOS
26.5.1 (25F80), Node 26.3.1, pnpm 9.15.0.
- Scenario: a minified `buildPluginApp` of the same temporary fixture
used by the regression: symlinked plugin root, workspace dependency
linked through `node_modules`, dependency `bb.pluginTailwindContent:
["src/**/*"]`, and a bundled `src/used.ts` symlink targeting
`actual-src/used.ts`.
- Method: one excluded warmup followed by 10 measured builds in one
process for each revision. Wall time came from `performance.now()`,
CPU/max RSS from `process.resourceUsage()`, and process peak RSS was
cross-checked with `/usr/bin/time -lp`.

| Metric | `origin/main` before | This PR after |
| --- | ---: | ---: |
| Measured builds | 10 | 10 |
| Mean wall/build | 7.319 ms | 9.849 ms |
| Median wall/build | 6.827 ms | 8.775 ms |
| p95 wall/build | 9.637 ms | 15.304 ms |
| User CPU, 10 builds | 99.675 ms | 111.322 ms |
| System CPU, 10 builds | 35.664 ms | 40.924 ms |
| Process max RSS | 200,400 KiB | 196,384 KiB |
| `/usr/bin/time` max RSS | 205,291,520 B | 201,162,752 B |
| Generated `app.css` | 2,274 B | 2,647 B |
| Dependency `.tracking-widest` | absent | present |
| Plugin `.leading-loose` | present | present |

The correctness delta is the emitted dependency utility (and its
required theme token), which explains the CSS-byte increase. These
sub-15 ms samples also include the extra filesystem lookup and CSS work;
the timing and peak-RSS differences are small-run noise, not a claimed
speedup or Safari runtime gain. This is a build-time correctness fix
only.

Fixes: N/A (no linked issue).

> AGENT GENERATED: by GPT-5 Codex
## Summary

- group update inventory by machine and keep provider, bb app, state,
badge, action, and changelog behavior coherent
- align Machines list and detail hierarchy with Updates using existing
settings rows, icons, menus, and tooltips
- add chrome-wrapped, navigable Settings and Updates Ladle coverage,
including all update states
- gate the changelog preview behind the existing experiments system
- keep the existing full experiments contract; no compatibility shim or
new endpoint

Supersedes get-bb#1933, get-bb#1934, get-bb#1935, get-bb#1936, and get-bb#1947 with one reviewable
change based on current main.

## Visual evidence

Exact base: 6b7c58f  
Exact head: 964709d

### Updates — desktop 1440×900

Before:

![Updates before at
1440×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/base-final-updates-1440.png)

After:

![Updates after at
1440×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/after-final-updates-1440.png)

### Updates — compact desktop 768×900

Before:

![Updates before at
768×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/base-final-updates-768.png)

After:

![Updates after at
768×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/after-final-updates-768.png)

### Machines — desktop 1440×900

Before:

![Machines before at
1440×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/base-final-machines-1440.png)

After:

![Machines after at
1440×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/after-final-machines-1440.png)

### Machines — compact desktop 768×900

Before:

![Machines before at
768×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/base-final-machines-768.png)

After:

![Machines after at
768×900](https://raw.githubusercontent.com/get-bb/bb/f87828332f6540f4c2f927511ab99dae487dd4be/evidence/settings-updates-machines/after-final-machines-768.png)

## Validation

- 56 focused app tests passed
- 36 focused server tests passed
- domain experiment tests passed
- 812 mobile tests and mobile typecheck passed
- plugin-registry generated-artifact test and typecheck passed
- Turbo typecheck passed for app, server, CLI, domain, server-contract,
and desktop
- exact main/head Settings → Updates and Settings → Machines states
checked at 1440×900 and 768×900

BB-Thread-ID: thr_qwah76hjqr

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

The compact prompt banner applied the 52px minimum needed by an active
pull request status plus its GitHub checks badge to every pull request
segment. Merged and closed pull requests render only the 16px state
glyph, so the extra reserved width appeared as empty space before the
git summary.

## What changed

- derive the GitHub checks-badge status once in the pull request display
module
- reserve 52px only when the status pill renders both glyphs, and 32px
for merged, closed, and other single-glyph states
- add an exact merged-pull-request-plus-committed visual fixture and
regression coverage for checked-open, merged, and closed widths
- no host-daemon wire, protocol-version, CLI, guide, configuration, or
documentation changes

## How you verified

- pnpm exec turbo run test --filter=@bb/app --
src/components/promptbox/banner/ThreadPromptContextBanner.test.tsx — 22
tests passed; the merged/closed matrix fails against the old
unconditional width
- pnpm exec turbo run typecheck --filter=@bb/app
- pnpm exec turbo run lint --filter=@bb/app — 0 errors; 144 existing
warnings
- dev-browser QA against the 320px compact fixture: the merged segment
shrank from 52px to 32px with no horizontal overflow, while the
checked-open minimum remained 52px
- captured matched before/after screenshots in the originating bb thread

Fixes: spacing regression reported in bb thread thr_tu5r5k62m2.

> AGENT GENERATED: by GPT-5
… focus glow

The glow first, because it was a bug rather than a taste call.
`.subscribe-input:focus` was declared twice. The second copy added
`box-shadow: 0 0 0 3px accent/18%` on top of the ring the form already
carried, so the field had two focus indicators — and on the dark canvas the
accent one bloomed into a coloured halo floating off the pill instead of
reading as focus. Both it and the neighbouring `border-color` / aria-invalid
rules are gone; the input had no border of its own for them to colour, so
they were inert as well as wrong.

The form is now two pills with air between them rather than one capsule with
the button inset. The fused shape was borrowed from the hero's install
command, but that control is a single object — a string you copy. This is a
field and a commitment. Separating them lets the field take the width it
needs and gives the button a shape a thumb reads as pressable. The field
becomes a recessed well, which is what makes the solid button beside it read
as the raised thing.

Both now sit inside a card: hairline ruled, heading pair centred above the
control. That composition is what lets the signup appear twice without the
second one reading as an afterthought — a bare form repeated does.

Second placement is directly under the hero window, so a reader convinced by
the mock does not have to scroll nine sections to act. It carries
`placement="hero"` against the closer's `"footer"`, and both values already
map through the CTA analytics, so the data can say which position actually
earns the address instead of crediting one arbitrarily.

Two instances meant the input id could no longer be a constant. Only the
closer keeps SUBSCRIBE_EMAIL_ID — it is what the blog and changelog anchor
at — and the hash-focus effect now runs on that instance alone rather than
both racing to focus the same field.

Also fixes a mobile regression from the previous commit: the feed's
`minmax(440px, 1fr)` laid a 440px track inside a 390px phone, because
auto-fill honours the floor even when the container is narrower than it. With
`min(440px, 100%)` the page is back to zero horizontal scroll at 430px.
…f agents

The hover convention was destroyed by the previous commit and this restores
it. Removing the dead `.closer-mark-btn` rules took two shared rules with
them, because `.closer-mark-btn` happened to be the LAST selector in each
list — so deleting `.closer-mark-btn:hover { … }` deleted the declaration
block its eleven siblings depended on, and both selector lists silently
merged into whatever rule followed. The 0ms-on-hover list merged into
`.trow, .side-act, … { transition: none }`, which is how every button, nav
link, footer link and PR row on the page lost its transition.

Both rules are rebuilt: controls carry `--hover-out` at rest and 0ms on
hover and active. Instant in, eased out over 150ms, as before.
`.subscribe-form:focus-within` is replaced by `.subscribe-input:focus` in
that list, since the ring moved to the input when the capsule was split.

The subscribe card takes `.rail`, so it lines up with the PR feed and the
stat cards instead of sitting inset by the 924px content box of `.wrap`.
That is also the real fix for the centring: `.rail` reaches 1200px by
computing a negative inline margin explicitly, and an earlier attempt using
`margin-inline: auto` could not work because `auto` has nothing to
distribute when the free space is negative — the card pinned left.

The footer loses the monospace uppercase. It was meant to mark the footer as
a different kind of surface and read as a costume instead; weight and ink
separate a group name from its links perfectly well. The rule above the
base row goes too — it could only ever be rail-width, so it read as a line
that stopped short rather than a division, and the section grounds already
do that job. A bb mark now sits right of the groups, masked rather than
placed, so the glyph takes the theme's own ink from one file. It is drawn
from bb-icon-dark.png because the light file has the rounded app-icon tile
painted into its artwork; the dark one is the bare letterform.

And the closer trades its single 72px mark for a field of the agents bb
runs: a shallow arc of provider discs, the two it is most used with sharp
in front, the rest falling back through two tiers of scale, blur and ink
until they are atmosphere. A line about many agents was being illustrated
by one mark of the tool. Flat discs rather than the glossy spheres of the
reference — depth comes from blur, scale and opacity, the things distance
actually does to a thing you are looking at, and not from gloss and rim
light, which would make it the 3D render this page does not allow itself.
…e that spans

The footer needed to separate from the closer. Two things had to be measured
before either worked.

`.slate` was the obvious answer and it does nothing here. `--band` is a 0.028
oklab step off `--canvas`, and the first attempt — a 4% ink mix — computes to
0.030. The same step. The "alternation" was the ground repeating itself, which
is why the edge measured two values out of 255 and read as nothing at all on
the dark canvas. 8% doubles it to roughly 0.06 in both themes: +8 sRGB in
dark, -12 in light, a section change you can see.

The rule is drawn in two halves that meet, because nothing else stays inside
the page. A pseudo-element stretched to -100vmax renders correctly and put
1232px of horizontal scroll on the document — `clip-path` clips paint, but the
box still counts toward scrollable overflow. Box-shadows never do, so the
wings are shadows: the line shadow sits behind, and the ground shadow is
pushed down by exactly its own spread plus one pixel, uncovering a single row
of the line beneath it. An outer shadow is clipped out from under its own
border box, so that cannot reach the middle span — `border-top` covers that,
and the two meet at the rail edge.

Verified as one continuous hairline: identical value at x=2 through x=w-3 in
both themes, and the document is back to zero horizontal overflow.
The field was eight discs placed evenly and read as eight discs placed
evenly. What makes the reference work is density — a carpet of shapes
overlapping and running off both edges, out of which a few resolve. So the
back layer is now a dozen anonymous discs, blurred well past the point where
a logo would survive anyway, and only the eight that are legible carry a
provider. That also settles the repetition problem: bb runs eight agents, and
eight is exactly how many marks appear. The whole field dissolves outward
under a radial mask, because a hard edge on a crowd that is meant to continue
gives it a boundary it should not have.

The front four carry a small bb mark on the shoulder, the way the reference
hangs a second avatar off each bubble. It says the thing the section is for:
that agent, running in this. It sits a step lighter than the disc it hangs
off rather than on raw canvas, which on a near-black ground read as a hole
punched in the shape instead of an object in front of it.

Motion, deliberately: three tiers drift at three speeds with the near one
travelling furthest, which is parallax rather than ornament, slow enough to
be weather, and off entirely under reduced motion. Nothing is stated by it.

The company rail stops wrapping. Eleven logos split 9 + 2 and the second row
was two centred orphans; flex has no equivalent of `text-wrap: balance` and
the count is prime, so no amount of balancing fixes it. The scrolling rail
with faded edges already existed for narrow screens and is simply the right
answer at every width, so it moves up and the breakpoint goes. `safe center`
is what makes it work in both directions — the row centres while it fits and
falls back to flow-start when it does not, so the overflow stays reachable
instead of stranded past the left edge.

And every artifact now sits on one rail. The hero mock took `--stage`
(1440px) on the argument that its three panes need the room, but it was the
only thing on the page at that width while the bento, the feed, the stat
cards, the signup and every section head sat on `--rail`. One element 240px
wider than everything else does not read as emphasis, it reads as a
misalignment. All seven measure 1184px at a 1280 viewport.

The nav mark loses its tile. `.bb-mark` loads bb-icon.png, the macOS app icon
with its rounded plate painted into the artwork — right where the app icon is
the subject, which is why the dashboard and the Telegram bot avatar keep it,
and wrong for a wordmark in the header. Masking bb-icon-dark.png gives the
bare letterform and lets it take the theme's ink from one file. The selector
is `.bb-mark.logo-mark` because `.dark .bb-mark` is two classes deep and a
single-class rule lost to it in dark mode.
Four passes of tuning size, density and blur could not make a crowd of grey
circles read like the reference, and the reason is structural: every bubble
there holds a different photograph, and that variety is most of what makes it
a crowd rather than a pattern. Monochrome marks cannot supply it.

Colour can, and it did not have to be invented. bb's own app already assigns
provider colours in `getProviderIconColorClass`
(apps/app/src/lib/provider-icon.ts): claude-code #D97757, pi #6D5DFB,
acp-opencode #2563EB, acp-omp #9333EA, and the foreground for codex,
acp-cursor and anything unmapped — correct, because those brands really are
monochrome. So a provider now wears the same colour here that it wears inside
bb, which is the point of this whole exercise, and four in colour against
four in ink is bb's own ratio rather than a palette chosen for a landing page.
They are declared once as tokens with that provenance written down. Colour
appears here and nowhere else: the company rail and the hero chips stay
desaturated.

The disc takes a weak wash of its provider's colour over the neutral step that
sets its distance — enough that four bubbles read as four objects, not so much
that the closer becomes a row of coloured blobs.

Sizes up about 60%, the carpet denser, the shoulder mark larger and properly
ringed so it sits in front of its bubble instead of looking like a bite taken
out of it, and overlapping discs get a drop shadow so equal values stay
separate.

Two containment bugs fixed on the way, both the same lesson in different
clothes. The stage needed `overflow: clip` rather than relying on its mask —
a mask clips paint, but the boxes still counted toward scrollable overflow and
put the document into horizontal scroll; the crop `clip` performs is the
effect that was wanted anyway. And the stage was `min(1040px, 100vw)` while
living inside `.closer`, which is inset from the viewport, so at phone width
it was wider than its own parent and hung 28px off the page — exactly the
subscribe card's bug, exactly the same fix. Zero horizontal overflow at 1280
and at 430.
… the phones

The colour was there and unreadable. A 14% wash of a brand colour over a
near-black canvas is mud — Claude's coral came out as a dirty brown circle and
Pi's indigo as a dull navy one, which is why the field read as having no
colour at all despite four of them being applied. The disc now takes the brand
as its whole ground, at a strength that survives the dark page, and the mark
is knocked out of it in canvas rather than painted on top in the same hue,
because a coral logo on a coral ground is invisible.

Providers bb keeps in ink — Codex, Cursor, Grok, Hermes — take a light neutral
disc instead of nothing, so the field alternates hue against value rather than
colour against absence. Half the crowd being grey was the other half of why it
looked colourless.

The shoulder mark was a hole: 43% of its parent, filled with raw canvas, on a
dark ground. It is 29% now and light, so it reads as a small object in front
of the bubble.

And there was a hard vertical seam down the right edge, where `overflow: clip`
cut a disc the mask had not finished fading. The gradient now reaches zero at
50% from centre — the box edge exactly — so the crop always lands on
transparent.

Separately: the phones in the bento now carry the same bottom dissolve the two
window demos beside them have. The device already ran off the bottom of its
card, so without it the phone ended on a hard cut while the cards above faded,
which was two treatments for one idea. Its stops sit lower than
`.bento-window`'s because the lower part of a phone screen is still content,
where the lower part of a window is chrome.
Window chrome, sidebar, thread header, composer and the Changes panel
rebuilt against the app's own source rather than approximated.
The hero patch rewrote the import block from its own base, where the closer
field did not yet exist, so it did not carry the type that field's per-disc
custom properties are cast through.
…h the rails

Five things, all reported from the rendered page.

The closer field is gone and so is the sticker that preceded it. The field
never worked, and the reason was structural rather than tuning: it recreates a
composition built on vivid photography, glossy spheres and real depth of
field, and this page allows none of the three — so every pass came out as mud
on the dark canvas or pastel on the light one. The mark goes with it; the
footer already carries one, and the closer's job is to land a CTA.

The composer and diff-toolbar selects were rendering as native macOS popup
buttons — the hero pass made them real controls, which the audit asked for,
but left `appearance: auto`, so the operating system's own chrome was painting
inside a recreation of the bb app. They now suppress it and draw the app's
chevron on the shared control metrics.

The build, gang and spawn demos were capped at 1040px inside the 1184px rail
the hero mock fills, so three of the page's four app windows were 144px
narrower than the first one a reader sees. They take the rail.

The bento pills sat a pixel high. Flexbox centres the line box, but a
sans-serif's cap height sits high within it and the descender space goes
unused on a word without one, so geometric centring reads as too high.

And the footer's ground goes back to the canvas. The 8% step separated it too
hard — the footer is the quiet end of the same page, not a different room.
The full-bleed rule stays and does the marking on its own.
The feed moves. I had made it a static grid of all eighteen, arguing the
volume was the argument — but a feed that never moves reads as a screenshot of
a feed, and the motion is what says this is still happening. The list renders
twice and the track travels exactly half its own height, so the second copy
arrives where the first began and there is no seam. The duplicate is
`aria-hidden` with its links removed from the tab order, so nobody is read or
tabbed through eighteen pull requests twice. It pauses on hover and on
focus-within, because every row is a link and a moving target is a hostile
one. Under reduced motion the track stops and the clip and mask lift, which
leaves the whole static list — the same thing a prerender and a screenshot
already got.

The fade is top and bottom now rather than left and right, so rows leave and
arrive through it instead of appearing at a hard edge.

Rows are instant in and instant out, with no ring on hover — just the tile.
That is the app's own behaviour: its list rows carry
`LIST_HOVER_TRANSITION = "transition-none"`, and a row that eases reads as
laggy beside a product that repaints immediately. They come out of the shared
hover convention, which is for controls.

The signup under the hero is removed; the closer keeps the only one.
Both bento chat cards drop their device and their client. What was there was a
drawn bezel wrapping a 240px screen wrapping a Telegram window — a title bar,
a wallpaper, a message field — around three message bubbles. Three layers of
furniture standing in for the thing itself, and they cost the conversation
more than half its width to say "this is a phone", which the content already
says by being a chat.

So: bubbles alone, at a size a reader does not lean into, and the ask card
alone with the two lines that give its question somewhere to have come from.
The Phone component, its status glyphs, the bezel asset reference and 120
lines of device CSS are gone, along with the `.phone-app` type overrides that
existed only to shrink this content onto a fake screen — removing those is
what lets the bubbles keep their natural size.

The bot types before it replies, which is what Telegram actually shows while a
bot works. Three dots on a stagger inside a bubble the same shape as the reply
that replaces it, so the reply reads as that message resolving rather than a
second one arriving; it collapses after handing off, because a permanent
typing indicator says the bot is stuck, and under reduced motion it never
appears at all.

`--tg-card` moves from `.tg-feed` to `.tg`. It is Telegram's own bubble
surface rather than a bb token — deliberately literal, like the outgoing
green — and it was scoped to the wallpaper element that no longer exists,
which left every incoming bubble with no ground.

Three company logos shipped opaque backgrounds inside RGBA files: notion.png
was a white square, and an audit of the rest found blackstone.png and
moodys.png the same way in black and navy. Only Notion was visible as a defect
because the page is usually read dark; the other two would have shown in
light. All three are keyed out by flood fill from the corners, so white or
black inside a glyph survives.
Extensions, Automations and Tasks rebuilt against the shipped plugin
manifests and the app's real panel shells.
The supplied artwork, reproduced verbatim, in the composition it was drawn
for: a slate shell holding three cards, each a near-black panel with an inset
inner panel and the plate centred in it, title and description beneath.

The panel is a fixed dark literal in both themes rather than a step off
`--canvas`, which is the one place this page allows that. These plates are
drawn with light strokes and near-black fills for a #0B0B0B ground; give them
a light ground and their depth ordering inverts and the whole illustration
falls apart. `--tg-card` already takes the same exception for Telegram's own
surface colour. The shell around them does re-tint, so the section still sits
correctly on either theme.

An earlier pass substituted real bb surfaces for the art on the grounds that
the page bans invented diagrams. That was the wrong call to make unilaterally
on someone else's page, and the art was supplied precisely to settle it.

The install row lands on the floor of the third card. A feature grid stacked
above a CTA buries it; a grid whose last cell ends in the CTA gives the
buttons somewhere to arrive. `margin-top: auto` against the stretched row puts
them on the floor all three cards share rather than directly under their own
paragraph, so the three plates read as a sequence that finishes on the thing
to press.

One row, no captions. A third of the rail is 402px and the pair wanted 423, so
both give a little instead of the row wrapping: the button loses some
horizontal padding and half a step of type, and the command truncates rather
than forcing width. The captions go because they repeat what the paragraphs
above them already say, and at column width they wrapped to three lines each.
The install row moves out of the third plate and back under the closer's own
paragraph, where the headline, the promise and the buttons read as one block
and the three plates below become what they are: three claims, not two claims
and a checkout.

The shell's ground goes from a 6% step to 13%. At 6% it was invisible, so each
card's title and description read as loose text on the page rather than as
part of the card they belong to — which is what made them look like they were
outside it. They were always inside; the container just could not be seen.

And the two chat cards get the inset window the other two demos have. Taking
the phone off left the messages floating directly on the card while the pair
above them sat in framed surfaces — one section, two treatments. Same border,
same ground, same bottom dissolve, so all four read as a window onto the
product rather than two windows and two loose fragments.
… the panes scrolling

Four reported bugs, each with a different cause.

The reply under the typing indicator dropped and then snapped back. The
indicator collapsed its own height on a keyframe, so every message beneath it
reflowed for the length of that animation and settled only when it ended. It
and the reply now share a single grid cell, which makes the handoff a
cross-fade in place: the reply defines the cell, the indicator sits inside it,
and nothing below either of them moves. The keyframe no longer touches layout
at all.

The subagent demo's divider appeared to shift between threads. It is
`.sub-main`'s left border, and the grid was `align-items: start`, so the rule
was only ever as tall as whatever that pane currently held — switching threads
changed the content, which changed the rule's length. Stretched, both panes
take the row's full height and the divider is a fixed edge.

The hero's transcript and Changes panel no longer scroll. Both fixtures are
sized to the window, so the scrollbars offered to move something that has no
more to give, and on a trackpad they captured the page scroll on the way past.

And the closer loses a box. It nested a slate shell around a near-black panel
around a softer inner panel; three borders deep reads as packaging. The middle
one is gone, the softer ground is now the plate's whole box at the height the
two of them shared, and the plates are larger inside it. The shell stays,
because the cards' titles and descriptions belong to a container — and it
re-tints with the theme, where the plate grounds stay literal for the artwork.
No band and no rule. The footer sits on the same canvas as the section above
it now, which is what it should always have been: it is the quiet end of that
section, not a separate room.

Everything that used to be here existed to draw a boundary between two things
that are not actually apart — an 8% ink band, a border-top, and a pair of
100vmax box-shadows whose only job was carrying that one-pixel rule out past
the rail where a pseudo-element could not go without putting the document into
horizontal scroll. All of it goes. Verified as a single continuous value down
the page margin, with no step at the seam.

The section grounds still do the dividing everywhere else on the page. There
was simply nothing to divide here.
The plate boxes were a hard `get-bb#111` in both schemes, on my argument that the
artwork is drawn for a near-black ground. That was true and still the wrong
call: a near-black card inside a light-mode page is a hole punched in the
section. The panel takes `--bg` now, the app-surface token, which lands white
against the shell's slate in light mode and a dark surface in dark.

The artwork has to turn over with it, so in light mode the plates invert. They
are drawn as light strokes over near-black fills; inverted, the fills become
near-white and the strokes near-black, which is exactly how the reference
renders them on its own light ground. Dark mode is what they were drawn for
and is left alone. The page already uses `grayscale(1) invert(1)` elsewhere
for art that cannot re-tint on its own.
Review, Build, Gang and Subagents rebuilt against the app's own source, and
the motion that did not correspond to anything the app does removed: the gang
transcript's 40ms ladder and its 500ms fade/blur/translate replay, the nested
row entrance, the Subagents child replay and its 420ms pane view transition,
the Build sidebar entrance with its 60/120ms child delays, the Review diff's
70ms per-line stagger and delayed second file, the commit arming colour
transitions on all three, and the storyboard timers and document View
Transitions behind them. Two transition rules remain in the demos and both are
control hovers, which is the convention.

Three conflicts, all resolved by taking what each side had actually decided
rather than a side wholesale:

- The phone rules and the `.gang-*.out` entrance classes collided. Neither
  survives: the phone rules went with the Phone component earlier tonight, and
  acts deleted the `.out` classes along with the ladders that used them. It
  emits no `.out` anywhere, in markup or CSS.
- The hover-convention list wanted `.review-commit` gone (acts, with the
  commit-arming transition) and `.subscribe-form` replaced by
  `.subscribe-input` (the focus ring moved to the input when the capsule was
  split). Both removals are correct, so both apply.
- The icon imports were additive on both sides; the union stands.

Verified after merging that nothing from tonight was undone: all four app
windows still measure 1184, the hover convention is 150ms out and 0ms in, the
feed rows are instant, the hero panes do not scroll, the subagent panes are
stretched, the chat cards keep their frames, the closer keeps its three
plates, and the footer keeps its bare ground. No horizontal overflow.
The rows in all three demo sidebars and the hero's measured 37 and 38px
against the app's 32. The cause was one wrong property repeated seventeen
times: `min-height: var(--row-h)` instead of `height`.

The app's row is `h-8 p-2 gap-2` with a 16px glyph
(apps/app/src/components/ui/sidebar.tsx:2173) — a fixed 32px box with the
padding inside it. As a floor instead of a height, 8px of padding above and
below a 21.6px line box stacked to 37.6px and the minimum never applied, so
the token was being honoured in name while every row it governed came out five
pixels too tall. The whole vocabulary was right; the box model was not.

Forty-seven rows across the hero, Build, Gang and Subagents now measure a
uniform 32.
…lap hid

The page had thirteen width values across ten distinct tiers — 1100, 1099,
960, 900, 820, 768, 767, 720, 700, 620, 520, 448, 430. That is not a system,
it is a record of thirteen separate decisions, and it is why a control could
be correct at 900 and wrong at 860. They collapse to four, each a real change
in what the layout can hold:

  1100  the hero mock leaves the text column and takes the rail
   900  every two-up becomes a one-up: bento, band, demos, closer
   720  desktop chrome gives way to mobile: rails scroll, stats reflow
   480  small phone: panels drop internals, controls stack

1099 survives as the exact complement of the min-width rule it pairs with.

The nav button growing at tablet widths was a symptom of that overlap: 820,
768, 767 and 700 all had rules in the same band, and merging them removed the
conflict. It is a flat 30px from 1440 down to where it hides now, and `.btn`
takes `white-space: nowrap` so a crowded bar can never break the label across
two lines again.

The demos stop stacking their sidebars. On a compact viewport the app closes
the sidebar and offers it as an overlay — `isCompactViewport ? openMobile :
state === "expanded"`, with a `data-sidebar-mobile-backdrop` — so it is never
above the conversation. The previous pass made the panes a swipeable
scroll-snap rail, which is a good mobile pattern and not one bb has; that is
the same class of invention the demos are not allowed. Below 900 the rail is
simply gone and the thread has the card. Build keeps its panel, stacked, since
the panel appearing is the entire argument of that demo.

The hero's install pair is capped at 380px. `width: 100%` alone let two
stacked buttons span 664px at a 720 viewport, which reads as a form rather
than a choice. The mobile ordering was already right and stays: the command
leads and the macOS button is demoted, because a macOS download cannot be
completed on the phone in the reader's hand.

One bug found while verifying: the `.sub-rail` hide sat above the base rule
that sets it visible, and at equal specificity the later declaration wins, so
it never applied. Scoped to `.sub-demo .sub-rail`.

Swept 1440, 1200, 1100, 1000, 900, 860, 800, 760, 720, 680, 620, 540, 480, 430
and 390: no horizontal overflow at any width, and nothing escapes an unclipped
ancestor.
Extracted to `CloserPlates` and left unrendered rather than deleted — the
artwork was supplied for it and the composition is finished, so restoring it
is uncommenting one line at the call site.
It was two blocks side by side, each with a caption under it — a filled
download button and a bordered command pill, four elements arguing for the
same act. Now it is one capsule: the command reading along the left, the
download inset in it on the right. That is the shape of a control you
complete, not a pair you choose between, and it is what the reference's
search-and-save bar gets right.

The captions go with it. "One-click, no terminal" and "Windows (via WSL),
Linux & remote machines" were doing real work while the two halves needed
telling apart, and none once the capsule reads as one thing — the platform
detail is in the command itself.

The hero's economics line moves above the control, where it reads as the
second half of the subtitle rather than a footnote to the buttons, which is
what it always was. Subtitle, terms, then the one thing to do.

Both halves keep their behaviour: the command still copies on click and still
reports `landing_cli_command_copied`, and the download still carries its
placement, so hero and closer remain separable in the click-through data. And
because it is one component, the closer picked it up without a second
implementation.

It comes apart below 720. The capsule only works as a pill while both halves
fit, and the action is a fixed 211px — by 540 the command beside it had about
120px and was truncating to nothing. Stacked and capped at 380, each half is
full width, and the command stays first, because a macOS download cannot be
completed on the phone in the reader's hand while the command can be copied,
sent, or run against a remote machine. Verified at 1200, 900, 720, 540, 430
and 390: no overflow, no truncation, both instances identical.
I built a pill and you asked for a tile. The capsule put the command and the
download on one line, which reads as a search field — the wrong promise for a
control whose two halves are two ways to get the same program.

It is a rounded card now: the section's subtitle top-aligned inside it,
because the card is the offer being made — what bb is, then the two ways to
have it — and the two actions across the bottom. The terms line stays outside
and below, where a footnote belongs.

The halves are exactly equal, and that needed grid rather than flex. Both
children reported `flex: 1 1 0` with `min-width: 0` and still came out 306 and
290: a flex basis of zero is a starting point content can argue with, while
two `1fr` columns are equal by definition. That guarantee is the point —
neither way of installing is the lesser one, which is the whole reason both
are offered, so neither gets the wider box. 298 and 298.

One component, both sections: the hero passes its subtitle, the closer passes
its own. Below 720 the card keeps its shape and only the pair inside stacks,
since two half-width buttons on a phone are two truncated buttons.

Swept 1200, 900, 720, 540, 430, 390: no overflow, both instances identical.
They were appearing after the conversation had finished, sitting on top of the
reply and covering the first two words of it.

The cause was a mismatch I introduced: the messages animate once
(`tg-in … 1 iteration, backwards`) and then stay, while I gave the indicator
`infinite` at 5.4s. So every 5.4 seconds it faded back in over a reply that
had been settled for good. A typing indicator that returns after the answer
is not a smaller version of the right idea; it is the opposite of it.

One shot now, held at zero by `forwards`, ending exactly as the reply it hands
off to begins. `pointer-events: none` as well, because it shares a grid cell
with a real bubble and invisible is not the same as out of the way.

Verified along the timeline: visible at 1.2s, gone at 4.2s with the reply
reading in full, still gone at 8s.
The single-layer version read as a grey slab, and three things were doing it.

One flat ground at 6% is not a card on a near-black page. The reference gets
its cardness from nesting, so this does too: an outer shell with a quiet
header row — a label left, the platforms right — and an inset panel holding
the offer and the two ways to take it.

The lead was left-aligned under a centred headline, which is most of why the
card looked pasted on rather than placed. It is centred now, on the hero's
own axis.

And the headline was sitting on the card. Measured, the gap was zero — the
h1's box ended exactly where the card began, so "builds itself" set its
descenders on the edge. It clears by 34px.

The inner panel takes `--bg` rather than the reference's light literal. That
file is drawn for a light page, where a near-white inset reads as paper on a
dark mount; unchanged here it would be a lit block in the middle of the hero.
The app-surface token nests the same way in both themes — the same correction
the closer plates needed.

Swept 1200 through 390: no overflow, both instances identical, halves equal
at every width.
The headline was breaking because of its container, not its type: `max-width:
12ch` held it to 367px, so "The IDE that builds itself" went to two rows at
every width including 1440. At 26ch it sets on one line wherever one line
fits, and `text-wrap: balance` is left to do the job it exists for — the
narrow widths where it genuinely has to wrap. The floor of the clamp drops to
38px so the single line survives further down before it breaks.

The copy control sat 19px inside the command button's right edge, because the
button centred its contents as a lump. The command reads from the left now
and the glyph is hard right on its own padding, which is where a button's own
control belongs rather than floating mid-row.

The closer gets the bordered room the subscribe card already had, so the
headline, the install card and the signup share one edge instead of three
different ones floating on the band. Border only: a filled box would compete
with the install card nested inside it.

That needed one correction to land. The subscribe card carries `.rail` for
when it stands alone, and inside a container that is already rail-width it put
its own border exactly on the room's — two edges at the same x, so the room
appeared to have none. Inside the room it fills the content box instead, and
sits inset by 25px.
The signup was inside the room and the room was rail-width. Both were wrong.
A bordered box the full 1184 around a 660px install card is a frame around
empty space, and putting the signup in it said the two were one act when the
second is a separate thing you may also do.

The room takes the install card's own measure, the signup sits below it on the
same 660, and the closer becomes a single centred column of matching edges.
Inside the room the install card drops its own width cap and reads as the
room's contents rather than a second box inside a first.
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.