The Steer modal was replaced by the steer bar on the run detail page,
so the per-card Watch and Steer actions are no longer needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors `fabro rm RUN_ID`. Surfaces a Delete item in the run detail
Actions menu for archived runs, opens a Headless UI ConfirmDialog,
calls DELETE /api/v1/runs/{id}, invalidates the boards.runs cache,
and navigates back to /runs on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lists captured artifacts grouped by stage and retry, with per-file
download links that stream from the existing artifact download endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render sub-second tool/command durations in milliseconds (e.g. 321ms)
instead of 0.3s. Round token counts to whole k/M. Wrap assistant token
metric in a tooltip showing the input/output breakdown. Allow Tooltip
labels to be arbitrary React nodes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bottom steer bar now actually calls the run APIs: Send posts a
steering message and Interrupt fires immediately as a button (not a
toggle). The Actions menu drops the redundant "Steer" item and its
modal composer; "Send interrupt" still fires immediately and "Send
steering…" still focuses the bar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Graph tab duplicated the Overview's diagram. Drop it and the
/runs/:id/graph route, and move the DOT source view to a dedicated
/runs/:id/source page reachable from the sidebar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces native title attribute with a portal-based Tooltip component on
the events-feed elapsed times and the run header's last-event timestamp,
showing the full datetime (e.g. "04/24/2026, 1:23:40 PM") on hover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two new items to the run-detail Actions dropdown, always
present and disabled when unavailable. Send interrupt is wired to
POST /api/v1/runs/{id}/interrupt and gated on running status. Send
steering… focuses the bottom-dock steer textarea via a new
forwardRef handle on SteerBar; gated on running status with no
pending questions. Bumps the separator after the new pair to render
whenever any later group exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the two floating overlays with a single full-width bottom
panel that's always present on every run-detail tab and renders
either the InterviewDock (when there are pending questions) or the
SteerBar (otherwise). The dock has a top border and bg-page so it
sits flat against the page above instead of floating with a gradient.
Strips the outer fixed/gradient/rounded wrappers from both
InterviewDock and SteerBar so they render as inline content inside
the new dock. SteerBar gets a max-w-4xl centered form, an outlined
textarea field, and a new Interrupt checkbox button (with a visible
amber-fill checkbox indicator) between the input and Send. Send and
Interrupt aren't wired to the steer API yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a single-row text input + Send button that pins to the viewport
bottom on every run-detail tab as a placeholder for the future
steering composer (will replace the modal). Renders mutually
exclusive with InterviewDock: the dock takes precedence on blocked
runs with pending questions, otherwise the steer bar shows. Reuses
the existing --fabro-interview-dock-clearance variable so consumers
that already pad for the dock pick up the steer bar's clearance too.
Send is intentionally a no-op for now until the steering API is
swapped over from the modal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Transcript view is filtered to a single stage, so measure each
event's elapsed time from that stage's started_at instead of the
run's created_at. The previous behavior folded the run's
initialization wait (sandbox build, clone, devcontainer setup) into
every per-stage timestamp. Falls back to RunSummary.start_time and
then created_at for stages or runs that never ran.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
This makes the advertised mid-run steering path real: users can send
append or interrupt steering messages through the API, CLI, and web UI,
and the worker delivers them to live API-mode agent sessions or buffers
them for the next session. The change adds the control protocol, session
interrupt machinery, workflow hub, server route/OpenAPI/client updates,
and UI feedback needed for the whole path.
### Plan Summary
- Add `SteerKind`/`run.steer` wire protocol and `POST /runs/{id}/steer`
- Deliver steers through subprocess JSONL or the in-process
`SteeringHub`
- Support append and interrupt behavior in agent sessions, with bounded
buffering and events
- Expose steering in the CLI/web UI and surface SSE toasts
## Flow
```mermaid
flowchart TB
UI["CLI / Web UI"] --> API["POST /runs/{id}/steer"]
API -->|"subprocess transport"| Control["Worker control JSONL"]
API -->|"in-process transport"| Hub["SteeringHub"]
Control --> Hub
Hub -->|"active API sessions"| Session["SessionControlHandle"]
Hub -->|"no active session"| Pending["Pending buffer"]
Pending -->|"first future API session"| Session
Session --> Agent["Session round loop"]
Agent --> Events["RunEvent stream"]
Events --> UI
```
## What changed and why
- Agent sessions now expose a lightweight `SessionControlHandle`, drain
steering at the top of each round, and use a replaceable round
cancellation token for interrupts. LLM waits are cancelled promptly,
while tool execution observes cancellation cooperatively so every
committed `tool_use` still gets a matching `tool_result`.
- `SteeringHub` owns active API session registration, broadcast
delivery, pending buffering, FIFO queue caps, and steering
lifecycle/drop events. A completion coordinator closes the
final-response race without introducing a workflow dependency into the
agent crate.
- The server route replaces the 501 stub, validates run state and
best-effort CLI-only steerability, and forwards through either
subprocess control JSONL or the in-process hub. OpenAPI and generated
clients now include the request type.
- The CLI and web UI can send append or interrupt steers. Run detail and
board views open the new composer, and shared SSE subscriptions now
support per-subscriber event callbacks so invalidation and steering
toasts can coexist on one EventSource.
## Review notes
- Steering actors stay on top-level `RunEvent.actor`; event props only
carry steering kind/drop metadata.
- Buffered steers replay as append messages to the first API session
that registers after an empty-active period. Per-stage targeting remains
out of scope.
- CLI-mode agent stages are still not steerable; the server returns a
best-effort 409 when all active agent stages are CLI-mode, while the
worker hub remains the authoritative safety net.
- No persistence or schema migration is required; active and pending
steering state is in memory.
- New tests focus on protocol round-trips, hub buffering/bounds, session
steering-loop behavior, SSE fanout, and basic server rejection paths.
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
### Summary
Billing and stage lists now use the event-sourced `RunProjection` as
their source of truth, so running and retrying stages appear immediately
and runtimes keep advancing in the UI. This removes the checkpoint
completed-node bypass that hid in-flight work and froze totals until the
next server response.
### Plan Summary
- Store stage `started_at`, terminal `duration_ms`, server-internal
`usage`, and lifecycle `state` on `StageProjection`.
- Populate those fields from stage lifecycle events, including retry
transitions and per-attempt reset on new starts.
- Render `/runs/{id}/stages` and `/runs/{id}/billing` from
`RunProjection.iter_stages()`.
- Expose the new API/client fields and tick in-flight billing runtimes
on the web UI.
```mermaid
flowchart TB
Events["Stage lifecycle events"] --> Projection["RunProjection StageProjection"]
Projection --> StagesAPI["GET /runs/{id}/stages"]
Projection --> BillingAPI["GET /runs/{id}/billing"]
StagesAPI --> StageUI["Stage sidebar/stages view"]
BillingAPI --> BillingUI["Billing tab live totals"]
```
### Key decisions
Retry and revisit handling stays one row per node id: latest visit data
wins, while first-seen event sequence keeps ordering stable with
finalize output. `state` is stored rather than derived so `Retrying` is
representable, and old serialized projections still work through the
`effective_state()` fallback. Billing `usage` remains server-internal
and is skipped on the wire; public schemas only expose the fields needed
by `/stages`, `/billing`, and the frontend live timer.
Added focused reducer, server retry/revisit, API round-trip, billing UI,
and event invalidation coverage.
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Drop a resync useEffect that healed activeIndex back to safeIndex —
safeIndex already clamped reads, so the effect only triggered an
extra render. Reuse the shared ErrorMessage from ui.tsx instead of
the inline copy. Drop a useMemo over a tiny per-render array.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the read-only BlockedRunNotice with a viewport-fixed dock that
lets users answer pending human-in-the-loop questions without dropping
to the CLI. Supports YesNo, Confirmation, MultipleChoice, MultiSelect,
and Freeform question types, plus the allow_freeform fallback for
choice-with-write-in. Multiple pending questions surface a "+N more"
pill so a parallel-handler run can be drained from one place.
The dock subscribes to interview.* SSE events for auto-refresh and
posts answers via the existing /runs/{id}/questions/{qid}/answer
endpoint. Cancel is consolidated into the page header (now shown for
blocked runs) so the dock chrome stays focused on the conversation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move favicon, logo, logotype, and PNG icons from /public/ root to
/public/images/ so the HTTP log middleware can drop them by path
prefix. Extends the existing /assets/ skip in http_log_middleware to
cover /images/ as well, removing favicon/logo entries from the server
log without filtering by extension (which would risk muting future
extension-suffixed API routes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Share ACTIVE_STAGE_STATES/SUCCEEDED_STAGE_STATES across stage-sidebar and
run-overview, collapse the nested match in active_stage_state_from_events,
and drop a few WHAT-comments that narrated the recent rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract Panel, Row, ViewToggle, and value renderers into a shared module
so the run settings page can adopt the same paneled layout and Settings/
JSON toggle as the server settings page. The run page groups its frozen
snapshot into Workflow, Sandbox, Git, and Artifacts panels and falls
back to raw JSON for everything else.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "Run Logs" entry to the run detail sidebar that fetches the
worker tracing log via GET /api/v1/runs/{id}/logs and renders it with
auto-refresh while the run is live. Refreshes the embedded SPA bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace React Router loader/action state paths with SWR query and mutation hooks.
Add targeted run and board EventSource managers that invalidate SWR keys, and refresh embedded SPA assets.
Removes the light/dark toggle infrastructure in favor of a single dark
theme. Deletes the theme context, boot script, light-mode CSS overrides,
logotype-light asset, and the pierre-light diff theme. Collapses
graph-theme into a single constant. Adds scheme-only-dark on <html> so
native controls and the server-injected Graphviz @media query render
dark.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expose cancel, archive, and unarchive from the run detail view,
surface blocked-question context, and route run-detail and run-files
notifications through a single shared toast provider.
This also refreshes the embedded SPA bundle and marks the lifecycle
actions plan complete.
- extract a shared CopyButton into components/ui.tsx and drop the
install wizard's local duplicate
- sticky stage header at the top of the turn stream so users always
know which stage they're reading as they scroll
- copy-to-clipboard button on System, Assistant, and Command blocks;
revealed on hover/focus
- stdout/stderr longer than 20 lines collapse to the last 20 with a
"Show N earlier lines" expander
- bump the [10px] labels in tool-use.tsx to [11px] for readability
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds apps/fabro-web/app/components/state.tsx exposing EmptyState,
ErrorState, and LoadingState on a shared StatePanel chrome so every
"the content isn't ready" surface looks like the same app.
Swaps in place of bare <p> tags and ad-hoc bordered divs:
- run-detail: "Run not found" is now an ErrorState
- run-stages: "No stages yet" is an EmptyState
- run-overview: empty-graph panel is an EmptyState
- run-billing: empty-billing panel is an EmptyState
- runs: filtered-empty ("no matching runs") now renders an EmptyState
(the branded landing empty is preserved as RunsLandingEmpty)
- install-app: session-loading StatusPanel replaced by LoadingState
No change to the root ErrorBoundary — full-page crashes stay there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- introduce --color-on-primary (navy-950 in dark, white in light)
so text on bg-teal-500 reads clearly regardless of mode; swap
hardcoded text-navy-950 occurrences on teal fills for text-on-primary
- darken --color-fg-muted in light mode from slate-400 (#94a3b8) to
slate-500 (#64748b); slate-400 failed AA on the tinted page
- deepen page tint to #eef2f7 and strengthen line/line-strong so
white cards have real edges, not invisible hairlines
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run-stages:
- replace the full-width tinted System/Assistant cards with a subtler
left-accent bar and header so dense streams read cleanly
- unify the Running/Timed out/exit/duration indicators under a shared
StatusPill, and bump stdout/stderr labels from 10px to 11px
- raise the selected stage header from text-sm font-medium to
text-base font-semibold
- command output preformatted text is now text-sm on mobile
run-overview and run-graph:
- extract the floating direction/fit/zoom controls into a single
GraphToolbar capsule with internal dividers, shared between both
graph views
- drop the translucent canvas in favor of solid bg-panel-alt
- wrap the "no workflow graph" message in a proper empty-state panel
run-billing:
- tfoot now uses bg-overlay so totals read heavier than the body
- table headers gain font-medium and a readable fg-3 instead of
fg-muted
- "By model" heading is a real section heading, not an eyebrow
- add an empty state when a run has no billing yet
stage-sidebar:
- cancelled stages now use NoSymbolIcon so they don't look identical
to failed stages at a glance
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract the wizard's INPUT_CLASS, PRIMARY_BUTTON_CLASS,
SECONDARY_BUTTON_CLASS, and ErrorMessage into
apps/fabro-web/app/components/ui.tsx so auth-login, setup, and the
install wizard share one source of truth.
- auth-login: raise the heading to text-2xl, swap white-on-teal for
navy-on-teal, replace the bordered dev-token input with the outline
pattern, use the ErrorMessage pill for invalid tokens, associate the
input with a label, and shrink the GitHub mark to size-4 per the
icons guideline
- setup: replace the nested bg-overlay cards with a numbered <ol>
matching the wizard's welcome layout, raise the heading, switch the
primary button to navy-on-teal
- install-app: re-import the shared primitives instead of holding
local duplicates
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Command stages were emitting events but the frontend only handled agent
turns, leaving the page empty. Parse command.started/completed events
and display script, stdout, stderr, exit code, and duration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stages API used checkpoint.current_node to identify the running
stage, but current_node is the last *completed* node — always already
in completed_nodes, so the running-stage check was always false.
Switch to checkpoint.next_node_id which correctly identifies the
currently-executing stage.
Also move SSE subscription from run-detail parent layout into the
StageSidebar component with since_seq=1 to replay all events and
close the race between loader fetch and SSE connection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract shared StageSidebar component from 4 duplicated implementations
across run-overview, run-graph, run-settings, and run-stages routes.
Add SSE subscription in run-detail parent layout so all child routes
get live stage updates — stages appear immediately when they start,
show a spinning icon while running, and display a ticking elapsed timer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>