Reuse shared frontend formatting and SSE dedupe helpers, tighten typed sandbox handling, remove obsolete run DTOs, and collapse auth-session revoke into a single store operation.
Replaces the /settings/live-events placeholder with a working page that
streams server-wide events from /api/v1/attach. Shares the leader-owned
cross-tab EventSource so additional tabs subscribe without opening
parallel connections.
The page keeps an in-memory ring buffer (newest first, max 1,000) with
id or run_id:seq dedupe and resets on remount; live-only by design,
nothing is replayed on connect or persisted in the browser. Reuses the
existing event-debug filters, search, and details panel, and links each
row's run_id to /runs/:id. The category filter is the static set of
DebugCategory values so "All types" always matches.
Settings layout is now fullHeight-aware so the events page can fill the
viewport alongside the sub-nav. DebugEventDetailsPanel's event prop is
broadened to a shared EventDisplayPayload shape so it accepts both
EventEnvelope and the live payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the separate sandbox record shape with a typed RunSandbox model shared by projections, API responses, and generated clients. The public contract now uses SandboxProvider plus a non-null id and working_directory, and removes sandbox identifier/name leakage.
Mirrors the affordance on the VNC panel. The button links to
/runs/:id/terminal, which renders the chromeless full-screen TerminalView
in a new browser tab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `/runs/:id/terminal` route renders a bare TerminalView at viewport
size, opened outside the AppShell so there is no nav, sidebar, or run
detail tabs around it. TerminalView gains a `chromeless` prop that hides
the toolbar and decorative wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the right column rendered the mode toggle on its own line
above each panel's status/action row, leaving an awkward double-row
header. Now each panel accepts a `leading` slot and hosts the toggle
inline with its existing status pill / breadcrumbs and action buttons,
so Terminal/Filesystem/VNC each occupy a single tight header row.
Also lands a quiet DEFAULT_DIR change in the filesystem panel from
/workspace to /, matching how non-clone sandboxes (and Daytona's image
layout) actually expose the working tree, plus matching test updates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Combines the run detail Sandbox and Terminal tabs into one. Sandbox
details sit in a narrow left column and the terminal fills the wider
right column, separated by a vertical divider that runs from the tab
nav down to the steer bar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders a Gantt-style strip below the Thread toolbar on agent and prompt
stages only. Bar position encodes start time and width encodes duration,
so empty stretches in the strip are real dead time (idle waits, sandbox
spin-up, time waiting on a human steer). Categories collapse to five
colors: system/agent/tool/user/interrupt. Tool/command bars use their
explicit durationMs; tool groups span first child's start to last
child's end so dead time between grouped tools is honored; assistant
bars span the gap from the previous activity's end to the message ts as
an approximation of "thinking" time; system/steer/interrupt are 4 px
instant markers. Hover shows kind/label/elapsed/duration in a portaled
popover; click selects the matching turn or tool group and shares state
with the existing event list and side panel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The outer scroll container had pt-2 with pb-[calc(1.5rem+dock)], so
content sat ~8px below the tab toolbar but ~24px above the dock —
visibly off. Each new renderer also re-applied its own pt-2, doubling
the top spacing. Bump the outer pt to pt-6 so it matches the 24px
baseline bottom, and drop the duplicated pt-2 from every renderer
wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten the meta bar (drop redundant labels, fold timestamp into the
duration tooltip), elevate the wait card to a centered hero clock,
strengthen the conditional view with the actual chosen edge and
condition expression sourced from run-level edge.selected events,
upgrade the fan-in selected card with a trophy badge and gradient, and
redesign the parallel stat strip with toned numbers and a duration
column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders a full-width strip below the Debug toolbar with one bar per
event placed by elapsed time. Hover shows category/name/elapsed;
click opens the event in the side panel. Collapses event categories
to five colors (agent/command/lifecycle/human/system) and shows the
event count persistently on Debug so the strip's density has a number
next to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the raw Debug fallback for human, conditional, parallel,
parallel.fan_in, stack.manager_loop, and wait stages with purpose-built
views — Q&A transcript, decision card, children grid, fan-in results
with reducer transcript, cycle summary, and live waiter clock. Add a
generic Summary card as the new default for any unknown handler so
future StageHandler additions get a usable view for free. Debug tab
remains as the escape hatch on every handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Click the title (or its hover-revealed pencil) to swap it for an input;
Enter or blur saves via PATCH /runs/{id}, Esc reverts. Trims whitespace,
no-ops blank or unchanged values, and surfaces server validation errors
through the existing toast system. The board and breadcrumb refresh from
the same SWR cache after a successful save.
Add InlineMarkdown component that tokenizes titles via marked's
Lexer.lexInline and renders code, strong, and em. Block syntax, links,
images, and raw HTML degrade to safe text — no dangerouslySetInnerHTML.
Applied to the run detail h2 heading and the run list row titles. Title
metadata, search, and the API/server data model are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the loose interview answer request payload with a discriminated OpenAPI union so generated clients enforce the wire contract. Surface structured HTTP error details in the web client and update browser, CLI, and server answer submission paths to use the typed variants.
Introduces /runs/:id/events showing all events for a run with the same
look as the stage Debug tab — toolbar with category filter and search,
plus a row-by-row list with an expandable details panel. Extracts the
shared event-list primitives from run-stages.tsx into a new
event-debug.tsx module so both views stay in sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>