Skip to content

feat: operations UI — useOperation, OperationProgress, header tray and attention (#895 PR2) - #979

Merged
markhayden merged 5 commits into
mainfrom
feat/operations-ui
Oct 16, 2026
Merged

markhayden merged 5 commits into
mainfrom
feat/operations-ui

Conversation

@markhayden

Copy link
Copy Markdown
Owner

PR2 of #895 (plan: .claude/specs/operations-progress/PLAN.md § PR 2). Builds on the operations primitive from #976. No flows are converted yet (PR3–PR6); this ships the browser surface they will use.

What's in it

  • useOperation(id, { bootId }) + startOperation(fetcher) (@makinbakin/sdk/hooks): follows one operation over the shell's single SSE connection and reconciles on bakin.reconcile. Highest rev wins; events from another boot never apply; a read answered by another boot latches restarted (a late old-boot answer is ignored); same-boot 404 → expired; failed reads → unreachable with 1/2/4/8/15s backoff, keeping the last view. startOperation throws OperationStartError carrying the route's own status and body.
  • OperationProgress (@makinbakin/sdk/patterns, SDK-owned beside the search patterns, because the private kit can't depend on SDK types). Composes Timeline, Progress, Alert, Button, StatusBadge, Collapsible and CopyButton; no new tokens. It shows:
    • the plan as a timeline, with a determinate bar only when the size is known;
    • quiet/stuck judged from measurable progress alone (output is shown as liveness);
    • the waiting overlay;
    • Cancel only while allowed, otherwise the reason;
    • an Alert for each non-happy state;
    • child operations nested under the first stage;
    • the activity log under Details;
    • a compact variant for the tray.
    • Story Components/Feedback/OperationProgress: CanonicalUsage + 22 states, all play-asserted; 5 visual baselines (desktop + mobile), approved.
  • Header operations tray: visible only while something runs or finished in the last 2 min. A Popover of compact rows with Cancel and an href link. Malformed list answers are ignored instead of crashing the header.
  • OperationsAttentionProvider (beside ApprovalsAttentionProvider): toast + OS notification for finished/failed (once per operation, safe under SSE replay) and stuck (once per stage), for web/cli/discord origins only. Never for cancellations, children, system work, or while on the operation's page.
  • Docs:
    • UI overview "Long-running work" with an example;
    • plugin server-contracts link;
    • style-guide.md;
    • .claude/knowledge/operations.md Browser section;
    • CLAUDE.md Operations entry.
  • Census registration for the two new host components.

Performance

Measured against main with bakin-bits-official pinned to its compatibility ref:

Bundle Change
Shared SDK chunk +~18 KB
Host initial JS +6.5 KB (tray, list hook, attention provider)
CSS unchanged

Ceilings were regenerated; the regeneration also tightened several unrelated ceilings.

Verification

  • Tests:
    • tests/hooks/use-operation.test.tsx (rev/boot guards, reconcile, expired, restarted + late old-boot answer, unreachable recovery);
    • tests/ui/patterns/operation-progress.test.tsx;
    • tests/host/operations-{tray,attention}.test.tsx;
    • 23 story plays.
  • Live check on an isolated boot at 1280px and 320px: no horizontal scroll, opening focuses Cancel, Escape returns focus to the trigger, no console errors.
  • Full suite: only the known local-only failures (6 compiled-binary tests killed on macOS; 3 readiness tests that pass once host assets are built).
  • ui:conformance --full: every stage up to repository tests passes locally; the later stages run in CI.
  • bun run build: all three binaries built. The compiled-binary smoke is gated on Linux CI.

🤖 Generated with Claude Code

markhayden and others added 5 commits October 16, 2026 14:11
…895)

useOperation(id, { bootId }) follows one server operation over the shell's
single SSE connection and reconciles from GET /api/operations/:id?children=1
on bakin.reconcile. A view replaces the held one only with a higher rev;
events from another boot never apply; a read from another boot latches
`restarted` and nothing after it applies; a same-boot 404 is `expired`; a
failed read is `unreachable` and retries with bounded backoff while keeping
the last view. Children are tracked by parentId and returned in the
parent's declared order. cancel() posts to the cancel route.

startOperation(fetcher) returns a converted route's { operationId, bootId }
and throws OperationStartError with the route's own status and body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one progress surface for long-running work, in @makinbakin/sdk/patterns
(SDK-owned beside the search patterns: it renders the SDK's operation
types, and the private kit may not depend on the SDK). It composes
Timeline, Progress, Alert, Button, StatusBadge, Collapsible and CopyButton
with no new tokens:

- the declared plan as a Timeline; the active stage carries a Progress bar
  that is determinate only when the size is known, with the value in words;
  items render as a nested timeline (reindex tables, sync files);
- quiet and stuck judged from progress alone via operationQuietState,
  output shown as liveness and never resetting the clock; a restart never
  also reads as stuck;
- the waiting overlay names the holder (stale holders say so);
- Cancel only while allowed, otherwise the reason ("Can't be cancelled",
  "Will finish — past the point of no return");
- an Alert for stuck, failed, partially applied, verification failed,
  cancelling, cancelled, expired, restarted and unreachable, with Re-check
  where the outcome is unknown;
- child operations nest under the parent's first stage (repair: children,
  then verifying);
- the activity log under a collapsed Details with copy;
- a compact variant for the tray and rows.

Storybook: Components/Feedback/OperationProgress, CanonicalUsage plus 22
state stories with play assertions. Five approved visual baselines
(desktop and mobile). Public API inventory and performance ceilings
updated with the maintainer's approval (+~15 KB minified in the shared
SDK chunk; host initial JS unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The header shows an operations tray only while something runs or finished
in the last two minutes: a Popover of compact OperationProgress rows with
Cancel (while allowed; a refused cancel says why) and a link to the
operation's href. The live list merges GET /api/operations with
operation.updated under the rev/boot guards, lists top-level operations only,
resets on a new boot, and ignores a malformed answer instead of crashing the
header.

OperationsAttentionProvider, mounted beside ApprovalsAttentionProvider,
toasts and sends an OS notification for finished and failed (once per
operation, surviving SSE milestone replay) and stuck (once per stage, judged
with operationQuietState on the registry's stuck milestone) — only for
web/cli/discord origins, never for cancellations, children or system work,
and not while the person is on the operation's page.

Host initial JS +6.5 KB (ceiling recorded).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#895)

UI overview gains "Long-running work" (startOperation + useOperation +
OperationProgress, phases, onRecheck, what the tray and attention already
cover); the plugin server-contracts page points at it; the style guide's
feedback rules name operations as the only long-running UI; the operations
knowledge doc gains the Browser section, map rows and tests; the CLAUDE.md
Operations entry names the browser surface.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… census (#895)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markhayden
markhayden merged commit 08bbf5c into main Oct 16, 2026
33 of 34 checks passed
@markhayden
markhayden deleted the feat/operations-ui branch October 16, 2026 22:18
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