Skip to content

Make Build plan explicit and show build status in one capsule - #508

Merged
MaggieAppleton merged 5 commits into
mainfrom
design-build-capsule
Oct 12, 2026
Merged

MaggieAppleton merged 5 commits into
mainfrom
design-build-capsule

Conversation

@MaggieAppleton

@MaggieAppleton MaggieAppleton commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Implements the approved build-plan jig (v2), slice 1 of 2. Stacks on #453.

What changes

  • Tabs never act. Opening Build no longer sends implementation:draft (auto-draft, shouldAutoDraft, draftKey, activation and autoSent are gone). Only Build plan drafts or starts a build.
  • Build tab is off until a build exists (or a press is under way), with the tooltip “Build the plan to see its tasks”. Same rule on the phone destination bar.
  • Build plan lives top-right, left of the people here, always visible to writers. When it can’t run it is inert with the reason in its tooltip: write the document first, answer N open decisions, accepted comments, Chopin isn’t running, or not judged ready yet. No hidden or queued states. Without a readiness judge (no LIVE_BUILD), a document with no blockers is ready.
  • After the press the slot morphs into a quiet capsule: Breaking into tasks… → Building · 3 of 8 (progress ring) → Built (green dot); 3 changes to build (amber dot; clock under LIVE_BUILD) → Building changes… → Build stopped / Build failed (warning) with Retry. It stays visible while Build is open and opens Build when clicked. A collaborator sees the same status, with no action unless they can act. No “sync” wording.
  • Twin glyph: one BuildGlyph (ring, dot, clock, warning) renders identically in the capsule, on the Build tab and in Build’s headline. The ring fills in lockstep. Dots are 9px and sit 4px from the word (Maggie’s tweaks).
  • Linked hover: hovering the capsule lights the Build tab, and the reverse.
  • Hand-off: on the first press only, the glyph travels from the capsule to the tab as it turns on. It uses transform only and is skipped under reduced motion.
  • Build view is read-only: a status headline and the tasks. It keeps only the recoveries that need its context (Return to planning, Cancel build, the local-agent command).
  • Phone: the capsule keeps only its glyph in the top bar. The same glyph badges the Build destination, and Build’s headline carries the words.
  • Server: LiveSnapshot.changes counts top-level blocks added, removed or changed since the last built source (tasks/changes.ts). BuildReadiness treats documents past readiness as ready.

One snapshot read (useBuildPlan) now feeds the capsule, the tab and the view. The capsule is self-contained in build-plan-button.tsx, so it can move to the two-bar header later.

Slice 2 (next PR): in manual mode (LIVE_BUILD off), record the last-built source and add a user-triggered Build for “N changes to build” that reuses queueRebuild, without the auto-trigger.

Tests

  • Unit: build-model.test.ts covers the look, labels, tooltips, tab readiness and the first-build steps. changes.test.ts covers the change count.
  • E2E: tab off before a build, a tab click never drafts, Build plan inert with open decisions, Build plan → capsule progression through the connector (Built, Build stopped + Retry, Building · 0 of 2), linked hover and click-to-open, and only Build plan drafting with the harness Planner.
  • Verified locally: implementation-launcher, harness.e2e and responsive-workspace pass. bun test has only the 3 failures already on the base (design-jig files).
  • check-design-contract: the only finding from this change is the reviewed-owner hash for workspace.tsx. It is not renewed here, per the cleanup rules.

Screenshots

Desktop: empty (tab tooltip), open decisions, ready, breaking into tasks, building, Build open, first build stopped, built, hover capsule → tab lit, hover tab → capsule lit, Build open, 3 changes (manual), 3 changes (live), building changes, failed, collaborator built, collaborator edited (live). Phone: ready, building, built open, 3 changes. Hand-off frames: before, in flight, landed.

Review fixes

  • The changes count is memoized per open document by plan revision and last-built source, and is computed only when the document is out of date. Reads between edits no longer re-parse the document under the plan lock.
  • The changedBlocks doc comment now says a reorder of any size counts only through the floor of one change.
  • An unknown builder is no longer called “the builder”. The tooltips read “Every pull request matches the document” and “Edits since someone else’s build”.
  • On a coarse pointer the capsule gets the shared invisible 44px target.
  • The design-contract hash for workspace.tsx was renewed in its own commit, after checking that its data flow is unchanged.

Review fixes (restack, 2026-10-12)

Phone: tapping the unavailable Build tab shows why above the tab bar

  • Decision 1. The capsule vocabulary wins. Polish the living document flow #457 holds the port of the Build view content, and I checked it against the spec: Since first build, the status detail line, no-change rows, task summaries, linkified blockers, warning dots, and the two-line title wrap.
  • Refusal copy. Refused starts, rebuilds and drafts go through one mapper (buildRefusalCopy and draftRefusalCopy), so raw server reasons never show.
  • Accessibility.
    • The capsule's tip is now an aria-describedby sr-only description.
    • State changes are announced in a polite live region. It has no status role, so the header keeps its "no status" contract.
    • Retry stays focusable, with aria-disabled and aria-busy while a press runs.
    • .build-glyph-spin only animates under prefers-reduced-motion: no-preference.
  • Full refusal text. It is available as the capsule's accessible description and wrapped tooltip, and in Build's headline. Build a delivered plan's later changes on demand without LIVE_BUILD #510 adds the inline alert.
  • Planner link.
    • postDraft refuses with planner-not-attached before queueing, so no build request is left behind; it carries the code to the browser.
    • Build plan is inert, saying "Connect a Planner to this document to build", when session:planner reports none attached and a draft is needed.
    • Readiness stays content-only, and starting drafted tasks, Retry and rebuilds are not gated.
  • Draft during edit (from Add Build plan and live sync status to the document toolbar #453). The Planner drafts once more, then says the document changed.
  • Phone. The Build destination is aria-disabled, not disabled. Tapping it, or an inert Build plan, shows the reason above the tab bar for four seconds in a polite live region.
  • Phone top bar. Build plan shows as its wrench glyph, so the top bar's title stays centred beside main's comments button. Main's phone clearance test now seeds a build before it opens Build.
  • Tests. The 500 ms negative waits are replaced by ordered-frame sync points, and aria-description checks by accessible-description checks. Main's phone tab-bar test now expects Build to be off until built. New phone e2e and model tests are added.
  • Design contract. The SurfaceToggle hash was renewed; its covered classes are unchanged.

🤖 Generated with Claude Code

desktop-01-empty

desktop-02-decisions

desktop-03-ready

desktop-04b-tasks-open

desktop-05-building

desktop-05b-building-open

desktop-06-first-failed

desktop-07-built

desktop-07b-built-hover-capsule

desktop-07c-built-hover-tab

desktop-07d-built-open

desktop-08-stale-manual

desktop-09-stale-live

desktop-10-rebuilding

desktop-11-failed

desktop-12-collaborator-built

desktop-13-collaborator-stale-live

phone-03-ready

phone-05-building

phone-07d-built-open

phone-08-stale-manual

handoff-1

handoff-2

handoff-3

@MaggieAppleton

Copy link
Copy Markdown
Collaborator Author

Review: Looks good (with fixes worth folding in before or with #510)

Checked out design-build-capsule (via #510's head, which contains it), ran it against the fake server on 8825, and drove every capsule state at 1440×900 and 390×844 with a stubbed snapshot, as the builder (e2e) and as a collaborator (mona). Diffed against living-build-ui, not main.

Verified

  • Tabs never act. Clicking, Enter and Space on the Build tab in the drafting, ready and built states sent no implementation:draft and no POST (checked on the WebSocket and the network). Build opens only once there is a build.
  • Inert Build plan. aria-disabled plus an sr-only reason. Reasons render as designed: “Write the document first”, “Answer 2 open decisions first”, “Chopin isn’t running for this document”.
  • Capsule wording matches the jig: Breaking into tasks… / Building · 0 of 2 / Built / 3 changes to build / Building changes… / Build stopped + Retry / Build failed + Retry.
  • Same glyph in every place. The capsule and the tab both render a 14px box with a 9px dot. The visible word-to-dot gap is 6.5px (4px gap plus the box inset), down from about 10.5px in the jig. That matches Maggie’s tweaks.
  • Linked hover works both ways (data-hot on the other end).
  • Hand-off. I captured about 54 frames, one every ~25ms. The glyph arcs from the capsule to the tab over --duration-linger × 1.4, moving by transform only, and the tab’s twin hides until it lands. Under reduced motion there is no flyer, and the tab glyph appears in place.
  • Server. The readiness change only affects the display flag. With no judge, buildReady is true. But the draft (draftRefusal, plannerWritable) and queueBuild (implementationReadiness, push/admin recheck via repositoryAccess) still refuse open decisions, accepted comments and non-writers. So nothing new lets a first build start with blockers or without write access.
  • workspace.tsx hash (lines 181/193/211/220/611). The diff only adds presentational props (buildGlyph, buildHot, onBuildHover), a class, data-hot, pointer handlers and the glyph span. There is no new state, effect, fetch or navigation path, so the data flow is unchanged and it is safe to renew.
  • Locally: bun run types passes, and bun test apps/server/src/tasks apps/web/src/build-model.test.ts gives 146 pass. CI’s validation job stops at the contract check, so CI never ran types or tests.

Findings

  1. Merge conflicts with Polish the living document flow #457 are severe. Polish the living document flow #457, on living-spikes, which sits on this PR’s base, changes build-view.tsx (+194), build-model.ts (+181), build-plan-button.tsx and room-workspace.tsx. This PR rewrites all four, and git merge-tree conflicts in all of them plus build-model.test.ts. Whichever lands second has to re-port the other’s behaviour by hand. Decide the order now. My suggestion: land this PR first and port Polish the living document flow #457’s polish into the new BuildLook model.
  2. Overlap with Add interactive task graph and local-agent hand-off #460 is medium. Add interactive task graph and local-agent hand-off #460 (main) changes workspace.tsx (the same Build destination), decision-view-control.tsx, room-workspace.tsx, DESIGN.md, local-launcher.md, implementation-launcher.e2e.ts (+230) and dynamic-web.json. That means a second hash renewal on workspace.tsx. It also adds an interactive task graph to the Build view, which this PR makes read-only. That is a product conflict as well as a textual one.
  3. Overlap with Phone comments list, Build empty state and a Chopin chip #495 is medium. Phone comments list, Build empty state and a Chopin chip #495 adds a Build empty state to build-view.tsx (+53), plus room-workspace, launcher e2e and icons/index.ts. With the Build tab off until there is a build, that empty state can never show. Phone comments list, Build empty state and a Chopin chip #495 should drop it.
  4. changes cost (tasks/builds.ts:liveSnapshot, tasks/changes.ts). Every GET parses the whole document twice and re-serializes every top-level block, inside exclusive(plan). useBuildPlan reloads once a second for every viewer while anyone types in a built document (EDIT_RELOAD_MS), so N viewers cause N full parses per second under the plan lock. Suggestion: memoize by (live.baseSource, plan.revision) on the plan, or compute only when outOfSync.
  5. changedBlocks moved-block case (changes.ts). A reorder has an identical multiset of blocks and is counted only by the Math.max(..., 1) floor. The test describes this as “moved counts as unchanged”, yet the capsule says “1 change to build” for any reorder of any size. That is acceptable, but the doc comment should say so.
  6. The collaborator fallback copy reads oddly (build-model.ts:builder). When builtBy is missing, the tooltip reads “· built by the builder” or “Edits since the builder built it”. Drop the clause instead of naming “the builder”.
  7. The phone capsule is a 32px tap target (.build-capsule[data-compact] uses --control-height-compact). It sits next to 44px controls. Use the coarse-pointer target size.

Screenshots (local only): every state at both widths, hover on each end, hand-off frame sheets with and without reduced motion.

MaggieAppleton added a commit that referenced this pull request Oct 12, 2026
The Build capsule (#508) keeps the Build tab off until a build exists and
explains what blocks Build plan in its own reason, so a blocked Build tab
on a phone no longer appears for a document that was never built. Drop the
phone empty state and its test rather than land a screen that is about to
become unreachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaggieAppleton added a commit that referenced this pull request Oct 12, 2026
The Build capsule (#508) keeps the Build tab off until a build exists and
explains what blocks Build plan in its own reason, so a blocked Build tab
on a phone no longer appears for a document that was never built. Drop the
phone empty state and its test rather than land a screen that is about to
become unreachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaggieAppleton added a commit that referenced this pull request Oct 12, 2026
The Build capsule (#508) keeps the Build tab off until a build exists and
explains what blocks Build plan in its own reason, so a blocked Build tab
on a phone no longer appears for a document that was never built. Drop the
phone empty state and its test rather than land a screen that is about to
become unreachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaggieAppleton added a commit that referenced this pull request Oct 12, 2026
The Build capsule (#508) keeps the Build tab off until a build exists and
explains what blocks Build plan in its own reason, so a blocked Build tab
on a phone no longer appears for a document that was never built. Drop the
phone empty state and its test rather than land a screen that is about to
become unreachable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton force-pushed the design-build-capsule branch 3 times, most recently from 3aa5751 to fc7364c Compare October 12, 2026 12:34
Base automatically changed from living-build-ui to main October 12, 2026 16:18
MaggieAppleton and others added 5 commits October 12, 2026 17:40
Opening the Build tab no longer drafts tasks; only Build plan in the header
does. Build plan is always visible to writers, inert with its reason until it
can run, then becomes a quiet capsule whose glyph is shared with the Build tab.
The tab stays off until the plan has been built. Live builds report how many
top-level blocks changed since the last build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e capsule's target

Reads between edits reuse the last count instead of reparsing the document under
the plan lock. A missing builder login is left unnamed. On a coarse pointer the
capsule gets the shared 44px target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under HARNESS=remote a one-click build's draft queued with no Planner attached
would never run, and the build request stayed set behind it. postDraft now
refuses with planner-not-attached before queueing, so the request is never
recorded, and the refusal carries its code to the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refused starts and drafts read as plain copy, including Connect a Planner to
this document to build while no Planner is attached; Build plan is inert for
that reason when drafting needs one. Edits made while tasks were drafted get
one more draft, then say the document changed. The capsule's tip is an
aria-describedby description, state changes are announced politely, Retry
stays focusable with aria-busy while a press runs, and the loader only spins
without reduced motion. On a phone, tapping the unavailable Build tab or an
inert Build plan shows the reason above the bar for a moment. The e2e tests
wait on ordered frames instead of 500 ms pauses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whitespace in the implementation routes, and a source-hash renewal for
SurfaceToggle in workspace.tsx, whose covered classes are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaggieAppleton
MaggieAppleton merged commit abcc49f into main Oct 12, 2026
3 checks passed
@MaggieAppleton
MaggieAppleton deleted the design-build-capsule branch October 12, 2026 17:00
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.

1 participant