Landing page elevation: real bb surfaces, section rhythm, and a rebuilt demo set - #6
Open
lnittman wants to merge 276 commits into
Open
Landing page elevation: real bb surfaces, section rhythm, and a rebuilt demo set#6lnittman wants to merge 276 commits into
lnittman wants to merge 276 commits into
Conversation
- 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.  ### After — foundation `3ad3d0f6c36b0b9b581360d0ca56d1c342b2b1a3` Viewport 1440 × 900 at DPR 2; image 2880 × 1800. Browser and Side chat occupy separate sidebar panes under the existing header.  ## 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.  #### Medium — stacked thread pages Viewport 1224 × 768 at DPR 2; image 2448 × 1536.  #### Compact — stacked thread pages Viewport 768 × 900 at DPR 2; image 1536 × 1800.  ### Tab-split configurations #### Wide — Browser and Side chat side by side Viewport 1630 × 900 at DPR 2; image 3260 × 1800.  #### Medium — Browser and Side chat side by side Viewport 1224 × 768 at DPR 2; image 2448 × 1536.  #### 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.  #### Ultra-wide — Browser and Side chat side by side Viewport 3440 × 1440 at DPR 1; image 3440 × 1440.  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:  After:  ### Updates — compact desktop 768×900 Before:  After:  ### Machines — desktop 1440×900 Before:  After:  ### Machines — compact desktop 768×900 Before:  After:  ## 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Elevation pass on the marketing site (
apps/web), built on the fork for review before anything is proposed upstream.The starting point was a page that described bb in prose and illustrated it with generic marketing furniture. The goal was a page that shows the product: every demo is either a real recording of a seeded workspace or a component rebuilt from the app's own tokens, and nothing on the page claims a capability bb does not have.
What changed
Sections and rhythm. Full-bleed alternating white/slate bands with the inner panels retained, no hairline dividers, no eyebrow captions. Header is non-sticky, matching production. The release callout is a pill.
Seven demo components, hand-built from the app. Tasks board, working-tree review, subagents, ask-a-question, plugin build, the multi-agent gang, and background spawns. Each one is a real bb window — the same chrome, rails, row metrics, glyph vocabulary, and state tints the shipped app uses. Several were verified field-by-field against live captures of a seeded instance rather than from memory.
Rendering law. Every demo is fully legible in prerender, with JS off, under
prefers-reduced-motion, and in a single screenshot. Motion enhances; it never owns whether content exists. Loops reserve their own space so no section changes height as it plays.Honesty law. Demo content comes from a seeded storefront workspace, never real user data. Stats (stars, forks, contributors, merged PRs) are fetched at build time by
apps/web/scripts/refresh-github-stats.mjsand baked. The merged-PR feed is real merges.Copy. Rewritten toward concrete mechanism over abstraction, with the em-dash habit removed — it was the page's strongest tell that a model wrote it.
Primitives. Button family reworked (no y-axis lift on hover), a copy control that morphs icon to checkmark with no tooltip, sanctioned type tokens, and theme colors derived from the
--canvas/--inkanchors rather than hardcoded literals.Verification
pnpm exec turbo run typecheck test build --filter=@bb/web— clean, 73 tests passing. Demos verified visually at desktop and mobile widths in a headed browser.What this PR is for
Review. It is open in the fork so it can be read publicly and critiqued in detail. It is not proposed upstream yet.