Add a `last_event_at` timestamp to RunProjection (set in apply_event so
every event ticks the field) and surface it through RunSummary and the
RunListItem board response. Backed by an OpenAPI extension so both the
Rust and TypeScript clients pick up the new optional field.
In the web UI, the run-detail header gains a "Last activity Xm ago"
badge next to the elapsed-time chip, driven by a 30-second ticker so the
relative time stays current between event refreshes.
The fabro-server tests.rs hunk is incidental rustfmt drift surfaced by
running `cargo fmt --all` over the workspace; including it keeps CI's
fmt-check green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an "All workflows" dropdown to the /runs toolbar between the
repo filter and the "Show archived" toggle, applied client-side.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an "All time / Today / Last hour / Last day / Last 7 days /
Last 30 days" dropdown to the /runs toolbar, applied client-side
alongside the existing search and repo filters in both Board and
List views.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prefix each run header chip with its icon (folder for repo, stack for
workflow, clock for elapsed) and surface the workflow name alongside
the repo so it's discoverable from the detail header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend GET /api/v1/boards/runs with include_archived=true (matching the
existing flag on listRuns), add an Archived BoardColumn that the server
appends only when the flag is set, and surface a "Show archived" toggle
on /runs that flips between request shapes. Default behavior is unchanged
— archived runs stay hidden.
Server: list_board_runs now takes ListRunsParams; board_column maps
RunStatus::Archived to BoardColumn::Archived; board_columns(include_archived)
appends the column conditionally. Two new handler tests cover the default
and flag-on paths.
Web: useBoardsRuns(includeArchived) keys requests so SWR refetches on
toggle; columnStatuses + columnStatusDisplay + columnStyles get an
"archived" entry; buildSkeletonColumns filters by the flag so the loading
state matches the eventual response. Two new buildBoardColumns tests cover
both column shapes.
Touched generated TS client files include unrelated whitespace drift from
openapi-generator-cli; including them keeps the working tree consistent
with what `bun run generate` produces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the row of contextual buttons (Steer / Cancel / Archive / Unarchive
/ Preview) on the run detail page with a single Actions dropdown. Each
action's pending label ("Archiving…", "Cancelling…", etc.) now appears on
its menu item; the trigger shows a spinner while any mutation is in flight
and disables itself to prevent stacked calls. The menu is hidden entirely
when no actions apply.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a toolbar with a multi-select level filter (TRACE/DEBUG/INFO/WARN/ERROR)
and a typeahead search to /runs/{id}/logs. Filtering is record-aware so
multi-line entries (stack traces, indented continuations) stay together with
their parent log line. Combine the panel header into a single row with
filters on the left and size + copy on the right; round byte sizes to whole
units.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a three-dot kebab menu to each runs board column header (in both
column and list view). The menu exposes a single "Archive all" action
that fans out individual archive POSTs for every archivable run in the
column via Promise.allSettled, then revalidates the board. A toast
summarizes full success, partial failure, or total failure. The menu
is hidden when a column has no archivable runs.
Tracked for a future single-POST API in #226.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macOS recursive fs.watch fires multiple events per logical save and emits
spurious "bubble" events for sibling directories. With the previous slow
~10s tailwind step those re-fires were absorbed between rebuilds; with
~60ms rebuilds the watcher entered a continuous-rebuild loop instead.
- Coalesce events with a 75ms debounce window so one save fires one
rebuild even when the editor produces several FS events.
- Filter to source-relevant extensions (ts/tsx/css/html/images/fonts);
ignore .DS_Store, .tsbuildinfo, swap files, and the extensionless
bubble events (e.g. "rename images") that were the dominant source
of the loop.
- Optional FABRO_BUILD_DEBUG=1 logs which path queued or skipped each
rebuild for future diagnosis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In --watch-web dev mode the server silently fell back to the embedded SPA
snapshot whenever the disk dist/ was missing or partial, so edits to the
web app appeared not to take effect with no error anywhere. This change
makes the dev loop visible and quick:
- static_files plumbs a dev_disk_only flag from RouterOptions.watch_web
into the fallback handler. When set, embedded fallback is skipped and
a miss returns 503 with a "build in progress" auto-refresh page.
- The web build script writes each rebuild into apps/fabro-web/.dist-builds/<id>/
and atomically replaces the dist symlink via rename(2), so requests
never observe a partially-populated dist tree.
- Tailwind is invoked through node_modules/.bin/tailwindcss directly
instead of bunx, removing a per-rebuild bun add @latest --force round-
trip and dropping rebuild time from ~10s to ~250ms.
- load_asset no longer falls through to the workspace dist/ when an
explicit asset_root is provided, restoring test isolation when a
real dev build is sitting next door.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Always route non-empty Files Changed views through Pierre's Virtualizer and worker pool, with full-height layout propagation and stable per-file cache keys. Copy Pierre worker assets during the web build so the static worker URL resolves in production.
## 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
Token scopes describe what *a run* is authorized to do, not server
identity. Today they live under
`[server.integrations.github.permissions]`, which can't be overridden by
`workflow.toml` / `project.toml` (server keys are stripped from
per-workflow layers) — so projects and workflows can't tighten or relax
permissions despite the docs already advertising a per-run config. This
PR moves them under `[run.integrations.github.permissions]`, where the
standard layer-merge (workflow > project > user > defaults) Just Works.
Greenfield, no migration shim.
## What changed
- **New layer/resolved types** in `fabro-config` and `fabro-types`:
`RunIntegrationsLayer`, `RunIntegrationsGithubLayer`, and resolved
counterparts. `permissions` becomes a flat `HashMap<String,
InterpString>` post-resolve; empty = no token requested.
- **Server schema**: `permissions` removed from `GithubIntegrationLayer`
/ `GithubIntegrationSettings`. `deny_unknown_fields` rejects the stale
path.
- **Bundled `workflow.toml` parsing** (`run_manifest.rs`): now goes
through `SettingsLayer` via the new `parse_run_layer_from_settings_toml`
helper, so stale `[server.integrations.github.permissions]` errors
instead of being silently dropped by the old `toml::Table` lift-out.
- **Consumers updated**: server preflight, run launch path, and the CLI
worker (`runner.rs`) all read run-level permissions. CLI worker
previously hardcoded `HashMap::new()` — runs launched via the local CLI
path were getting no `GITHUB_TOKEN` regardless of TOML.
- **Shared helpers** on `RunIntegrationsGithubSettings`:
`is_token_requested()` and `resolve_permissions(lookup)` so server and
CLI don't drift.
- **OpenAPI + TS client** regenerated; new `RunIntegrationsSettings` /
`RunIntegrationsGithubSettings` schemas added, `permissions` removed
from `GithubIntegrationSettings`.
- **Repo workflows + docs** rewritten to the new path. Docs gain a
security-model note (boundary = installation grants; no Fabro-side cap).
## Key design decision: hand-rolled `Combine` for
`RunIntegrationsGithubLayer`
`ReplaceMap`'s "empty inherits from below" semantics (`maps.rs:76-80`)
are wrong here — we want `permissions = {}` in a higher layer to act as
an explicit clear. So the layer field is `Option<HashMap<...>>` with
hand-rolled `Combine`:
| Higher layer | Lower layer | Result |
|---|---|---|
| `None` | anything | lower (inherit) |
| `Some(map)` | anything | `Some(map)` (full replace, including
`Some({})` = clear) |
Not derived: the blanket `Option<T: Combine>` impl would recurse into
the inner `HashMap` and reintroduce empty-fallback. Documented inline in
`layers/run.rs`.
`InterpString` is preserved through resolve and only flattened to
`String` at the start-services boundary, matching the existing pattern.
### Plan Summary
- New `[run.integrations.github.permissions]` layer + resolved types;
remove from server side.
- Hand-rolled `Combine` so empty-wins-as-clear; no change to
`ReplaceMap` semantics for other consumers.
- Strict `SettingsLayer` parse for bundled `workflow.toml` so stale
schema errors loudly.
- Both server and CLI worker paths read run-level permissions via shared
helpers.
- OpenAPI + TS client regenerated; parity test added.
- Repo workflow TOMLs and `integrations/github.mdx` rewritten.
### Fabro Details
<details>
<summary>Ran 0 stages in 61m 23s for $53.41</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **61m 23s** | **$53.41** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (12 nodes and 15
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1 && cargo dev docs refresh 2>&1 && cargo dev docs check 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings, test failures, and generated docs errors.", max_visits=3]
fmt [label="Format", shape=parallelogram, script="cargo +nightly-2026-04-14 fmt --all 2>&1", max_retries=0]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> fmt [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
fmt -> exit
}
```
</details>
⚒️ 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>
## Summary
Stage detail now loads activity from a canonical stage-scoped events
endpoint instead of falling back to the first 1000 run-wide events. This
fixes empty panes for late stages in long runs and removes the
presentation-shaped `StageTurn` API from the wire.
### Plan Summary
- Add `GET /runs/{id}/stages/{stageId}/events` with cursor pagination
and server-side `node_id` filtering.
- Replace frontend stage-turn/fallback loading with paginated
stage-events loading and local event-to-activity projection.
- Broaden SSE/SWR invalidation so every activity event consumed by the
reducer refreshes the per-stage cache.
- Remove `StageTurn` schemas/client models and update demo fixtures plus
pagination/handler/reducer tests.
## What changed and why
The store now scans the run event prefix and filters by `node_id` before
applying the `limit + 1` cutoff. That preserves sparse late-stage
matches that would otherwise be dropped if we reused the run-wide
limited scan and filtered afterward. The real-mode handler returns an
empty page for an unknown stage id in an existing run, while preserving
404 for missing runs.
On the frontend, `run-stages` fetches all pages for the selected stage
and feeds them through `eventsToActivity`, keeping `TurnType` as a local
presentation model. Invalidation now targets `runs.stageEvents(runId,
stageId)` for lifecycle and reducer-consumed activity events
(`stage.prompt`, agent messages/tools, and command events), so active
panes refresh from the existing run event subscription.
The OpenAPI document and generated TS client now expose
`listStageEvents` and drop stale `StageTurn` models. Demo mode serves a
`detect-drift` stage-events fixture using the same cursor semantics as
the real endpoint.
## API notes
`/runs/{id}/stages/{stageId}/turns` is removed; clients should use
`/runs/{id}/stages/{stageId}/events?since_seq=&limit=` and project
events locally. The `stageId` path segment for this endpoint is the
workflow node id, not the visit-qualified `node_id@visit` form used by
command logs/artifacts.
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Make BoardColumnDefinition.id reference the existing BoardColumn schema and carry that typed contract through generated TypeScript, server responses, demo data, and the runs board UI.
Reset coordinator state when the last subscriber leaves, clear pending debounce timers on close, and keep coordinated EventSource construction owned by the coordinator while fallback subscriptions keep their local factories.
Use unknown.ts helpers in parseMessage, factor out parseLeaderPair/Triple
and per-variant parsers to remove repeated typeof guards. Extract
leaderIsFresh() for the staleness check used in three places, and make
RecentEventCache amortized O(1) by walking expired entries from the
oldest instead of scanning the whole map per event. Drop the
closeOnTerminal parameter in run-events; the fallback path computes
close at its single call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prune stale election candidates as generations advance, reset coordination availability on explicit close, and keep fallback subscribers tracked so coordinator shutdown can clean them up consistently.
Stop coordinated election and leadership work when BroadcastChannel posting fails, so tabs degrade cleanly to per-subscriber fallback without stale resync or heartbeat side effects. Expand election coverage for the edge cases called out in the coordination plan.
Elect a single browser tab to own the global attach stream and broadcast run events to sibling tabs. Keep the existing per-tab EventSource path as the fallback when cross-tab coordination is unavailable.
Submitted and Queued lifecycle statuses now live in a dedicated Queued
column rendered to the left of Initializing; Starting stays in
Initializing. The column is omitted from the board when it has no items
so day-to-day boards stay compact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Render kanban column shells while board/auth/system queries load, so the
"Your runs will appear here" panel no longer flashes before runs arrive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Use BilledTokenCounts::default() for the non-LLM branch, hoist the
by-model stage count and hasLlmStages predicate out of JSX, and drop
the in-test for-loop in favor of iterator-based assertions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Include completed stages without LLM usage in run billing responses so command-only runs still show runtime rows. Keep token and model aggregates scoped to billed LLM usage, and render placeholder values in the web billing table.
Use the archived status's prior terminal kind so the Exit node keeps
its succeeded/failed fill instead of falling back to the default
transparent server fill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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>
Replace local CommandTermination/CommandOutputStream literal unions with
the generated enums from fabro-api-client, drop `as` casts and the `id!`
non-null assertion in run-stages, flatten the 6-deep status ternary into
streamStatus(), and use fabro_util::time::elapsed_ms in handler/llm/cli.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Represent command termination explicitly across sandbox results, events,
run projections, API types, and the run stage UI. This removes the fake
-1 exit code path for timeout/cancel and lets consumers tell cancelled
commands apart from timed-out commands.
Persist command stdout/stderr through scratch logs and finalized CAS refs, expose byte-offset tailing through the API, and render separate streaming panels in the web run view.
Resolve command output blob refs for execution-time consumers such as edge routing and retros, and make Docker streaming timeout/cancel drain output before returning.
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>
Return degraded run files with the same FileDiff[] shape as live responses, using nullable contents and per-file unified patches so the web sidebar and deep links work consistently.
When GitHub redirects back to /setup after installing the app, render a
distinct view that confirms the install and points users to retry the
run, instead of the first-time terminal setup instructions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Place a Settings | JSON toggle on the right of the description row. The
JSON view renders the full server settings object as syntax-highlighted
server-settings.json via the existing CollapsibleFile component.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuse the existing @pierre/diffs Shiki highlighter and registered DOT
grammar (already used on the workflow definition page) so the Source
view renders workflow.fabro with proper highlighting instead of plain
monospace text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>