Extracts inline components into apps/fabro-web/app/routes/run-files/:
- placeholders.tsx — sensitive/binary/symlink/submodule/truncated +
DegradedBanner + pickPlaceholder priority resolver
- states.tsx — EmptyState, LoadingSkeleton, InlineErrorBanner, Toast,
RunFilesErrorBoundary, emptyStateCopy, deriveEmptyKind
- toolbar.tsx — Toolbar with freshness + Refresh + Split/Unified
toggle, 44×44 touch targets
- keyboard.ts — useFileKeyboardNav with j/k nav + Enter/Space click
Adds:
- P2-3: consumes parent runStatus via useMatches to derive the 4-
variant R4 empty-state taxonomy (starting / no_changes /
failed_before_checkpoint / diff_lost) plus an "unknown" fallback
when the loader returned null.
- P2-4: RunFilesErrorBoundary handles 401/403 (access denied),
429/503 (inline retry affordance), 500 (parses request_id out of
the response body and surfaces it in the copy so users can cite
it when contacting support).
- P2-5: Refresh button now disables when the server reports the
same to_sha as the last successful fetch — no new checkpoint, no
point firing another request.
- P2-6: InlineErrorBanner for mid-session revalidation failures so
the user doesn't unmount to the route ErrorBoundary on a transient
SSE-triggered revalidation blip.
- P2-7: "No changes in this run" toast when a revalidation empties
the previously-populated list (files reverted upstream).
- P2-8: @pierre/diffs Virtualizer wraps file lists > 20 entries so
large runs don't synchronously mount every diff.
- P2-2: Split/Unified toggle with localStorage persistence
(fabro.run-files.diff-style). Below md (<768px) the toggle shows
the forced "unified" state but doesn't overwrite the persisted
desktop preference.
- P3-1: Enter/Space on a focused file row fires a click so
@pierre/diffs expand handlers (if any) take over, and the deep-
link handler now clicks the resolved row after scrolling to
trigger the same expand.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Empty-list hint in `fabro ps` now mentions archived explicitly so users
discover the new surface (plan Unit 7 follow-up).
- apps/fabro-web runs.test.ts gains an `isRunStatus('archived')` +
`runStatusDisplay` assertion so the web UI type stays in lockstep with
the Rust enum.
- `operations::rewind` now requires callers to pass `current_status`
rather than silently skipping the archived guard when absent, closing a
silent-bypass hole.
Scenario coverage for the plan's R14 read-only-on-archived contract over
HTTP:
- archived_runs_reject_mutations_with_actionable_body drives a run to
succeeded, archives it, then asserts 409 on /cancel, /pause, /unpause,
/start, and /events with the actionable 'fabro unarchive' body.
- appending_run_archived_event_directly_is_rejected covers the widened
denylist on append_run_event.
- archive_returns_404_for_unknown_run proves the RunNotFound mapping.
- list_runs_respects_include_archived_flag exercises Unit 5's listing
filter.
Also adds inline server.rs tests that pin the spec/router behavior at
the unit layer and documents that rewind.rs now requires callers to
pass current_status (already threaded through from the CLI and scenario
tests).
Single #[test] that exercises the full CLI archive flow: run a dry-run to
succeeded, verify ps -a shows it, archive, verify default ps hides it and
ps -a shows archived, unarchive, verify the prior terminal status is
restored, then re-archive and rm to confirm archived runs remain
delete-able (plan Scope Boundaries).
Adds the CLI-layer integration coverage the archived-run plan called for
in its Unit 6 test scenarios but never landed: help snapshots, required-arg
handling, happy paths (including ps/ps -a visibility switching), precondition
errors (archive on active runs, unarchive on not-archived runs), unknown-id
errors, idempotent no-ops, JSON output shape, and mixed-batch per-id error
aggregation. 15 new tests across archive.rs and unarchive.rs mirror rm.rs's
fabro_snapshot style.
Adds lib/crates/fabro-server/tests/it/api/run_files.rs covering the
HTTP-level plumbing branches of GET /api/v1/runs/{id}/files:
- Invalid run_id path returns 400
- Unknown run returns 404 (IDOR-safe; same status as missing-run case)
- Malformed from_sha / to_sha query params return 400 before any work
- Non-default from_sha value returns 400 even when hex-well-formed
(v1 reserves the parameter for a future version)
- Submitted run with no sandbox record returns empty envelope
- Demo mode (X-Fabro-Demo: 1) returns the 3-entry fixture without
touching the run store, with at least one populated-content entry
- Response envelope shape matches PaginatedRunFileList contract:
data: FileDiff[], meta: { truncated, total_changed, ... } with
correct field types
Sandbox-path happy case (live diff) and degraded-fallback scenarios
stay covered by unit tests on stitch_file_diff, build_fallback_response,
and the sandbox_git helpers, since integration-level scheduler setup
for terminal-run tests is flaky without broader harness scaffolding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nothing behavioral — each change is what clippy asked for:
- fabro-test: wrap the three polling-helper thread::sleep calls in a
single poll_sleep() with an #[expect(clippy::disallowed_methods,
reason = …)] since the helpers are deliberately blocking
- fabro-test: server_log_files now uses Path::extension() with
eq_ignore_ascii_case("log") instead of a case-sensitive ends_with
- fabro-workflow: import default_storage_dir rather than calling it
through its full module path
- fabro-cli/server/record: same absolute_paths fix
- fabro-cli/main tests: use a `use tokio::runtime::Runtime` to stop
referencing `tokio::runtime::Runtime` by full path
- fabro-cli/tests: replace three `as u32` casts on as_u64() results
with u32::try_from(...).expect(…)
- fabro-cli/tests: six `format!("...", var)` assertions switched to
the inline `{var}` form clippy prefers
Full verification passes: fmt, clippy, cargo nextest (4141 tests),
bun typecheck, bun test (40 tests), bun build, SPA embed diff clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P2-11: Assert RunFilesMetrics::emit writes ONLY the allowlisted field
set (run_id, file_count, bytes_total, duration_ms, truncated,
binary_count, sensitive_count, symlink_count, submodule_count, message).
Uses a tracing-subscriber Layer with a Visit impl that captures every
field name emitted under the run_files target; fails the test if any
non-allowlisted field appears. Catches future refactors that might add
paths/contents to the log line.
P2-13: Assert that when the first coalesce caller is cancelled mid-
materialization, the spawned task continues to completion and a
subsequent caller still receives the shared result. Proves the
tokio::spawn-based design survives request dropout without
re-materializing the diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the sensitive-path denylist, sandbox-git env helper, and metrics
emitter into a dedicated run_files_security module so the Run Files
Changed endpoint has a single, testable surface for security controls.
Denylist upgrades to globset::GlobSet with two explicit lists:
- Basename globs: .env, .env.*, *.pem, id_rsa, id_rsa.*, id_ed25519*,
*.p12, *.keystore, *.key
- Path-suffix globs: .aws/credentials, .git/config, .ssh/**
Matching semantics explicitly pinned:
- Case-insensitive via lowercased normalization
- Path traversal (`../`, `./`, leading `/`) stripped before match
- Basename globs match the final segment only — prevents
`log/.env_audit/data.txt` from matching `.env.*`
- Empty/pathological paths fail closed (sensitive=true safe default)
Also ships:
- sandbox_git_env() returning the env-hardening map
- RunFilesMetrics struct + emit() so tracing never leaks paths/contents
Handler migrates to consume the new module; inline denylist and inline
info!() call removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sandbox path: issue a best-effort `git show -s --format=%cI <to_sha>`
against the reconnected sandbox to resolve the commit time of HEAD,
parsed into chrono::DateTime<Utc>. Failures (command error, non-zero
exit, unparseable output) return None so the handler still succeeds;
the client simply won't show a "Checkpoint Xm ago" label.
Degraded path: populate meta.to_sha_committed_at from
projection.conclusion.timestamp (the run-end time). The patch was
captured then, so it's a reasonable proxy for "captured X ago" in the
UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings in the CLI storage/logging refactor (858e8e127), bootstrap
helper collapse (a256c14a7), and shared server-lifecycle test helpers
(f8c560a9a). No overlap with the web UI work on this branch.
Three P1 bugs from code review:
P1-1: Modified and renamed files now return real before/after contents.
Previously the handler fetched only each entry's new_blob and duplicated
that single blob onto both sides, so every modified file rendered as a
no-op diff in MultiFileDiff. The fetch path now collects both old_blob
and new_blob, deduplicated, into a single batched `cat-file --batch`
call and stitches contents back via a SHA->contents table. Added
regression tests for modify and rename.
P1-2: Degraded-patch denylist now matches both `a/<old>` and `b/<new>`
sides of each `diff --git` header. A sensitive file renamed to a benign
path was leaking its patch body through the fallback branch. Added
regression test with `.env.production -> docs/NOTES.md`.
P1-3: Sensitive classification now runs BEFORE the 200-file cap, per
the plan's R31-before-R27 ordering. Sensitive entries no longer evict
real changes when the cap is hit. Replaced the (fetch, prebuilt) Vec
pair with a single ClassifiedEntry-ordered list so response ordering
matches git diff --raw output.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Four fixes from post-merge review:
- Freeze archived runs on the remaining write surfaces the Unit 4 audit
missed: `put_stage_artifact`, `put_sandbox_file`, and `write_run_blob`
now all call `reject_if_archived` so a client cannot mutate artifacts,
sandbox files, or blobs on an archived run.
- Widen the `append_run_event` lifecycle denylist to cover every event
with a dedicated operation endpoint: archive, unarchive, and the three
control-request events (cancel/pause/unpause). Worker-emitted lifecycle
transitions and rewind's `RunRewound` / `RunSubmitted` replay still flow
through the endpoint as before.
- Map `fabro_store::Error::RunNotFound` to a distinct `Error::RunNotFound`
at the operations layer so the archive and unarchive HTTP handlers return
a 404 on unknown run ids instead of collapsing into a generic 500.
- Centralize the archived-run guard in `operations::rewind` by threading
`current_status` through `RewindInput` and calling the new
`ensure_not_archived` helper alongside a shared canonical error message.
The CLI caller drops its ad-hoc string comparison in favor of the typed
status it already loads from the server.
Phase 2/3 of the std::fs lint initiative (Phase 1 refactors landed in
commit 9d1c0d98c).
clippy.toml additions (appended to disallowed-methods):
std::fs::read, read_to_string, write, read_dir, copy, canonicalize
std::fs::File::open, File::create, File::create_new
std::fs::OpenOptions::open
File::options was deliberately excluded — it returns an OpenOptions
builder with no syscall. OpenOptions::open is where the block happens.
Non-blocking std::fs items (metadata, exists, create_dir_all, remove_*,
rename, and all std::fs types) remain legal.
Annotation policy (per updated plan):
- Mixed async/sync production source: function- or statement-scoped
#[expect(...)] so future accidental Tokio-path regressions in the
same file still fire.
- Fully-sync production source, test modules, integration tests,
build.rs: file-level #![expect(...)].
- Every #[expect] has a specific reason identifying the sync context.
Annotations added in ~90 files across the workspace. Notable narrow
placements: fabro-server server.rs current_server_target,
build_disk_usage_response, create_test_app_state_with_session_key;
fabro-server install.rs read_to_string rollback snapshot;
fabro-sandbox local.rs list_recursive; fabro-agent cli.rs FOLLOW-UP on
the JSON-stdout writer; fabro-llm providers/common.rs FOLLOW-UP for
load_file_as_base64 (7 translator call sites; revisit if file:// URL
usage grows).
build.rs blanket allows: fabro-api/build.rs, fabro-util/build.rs.
Pre-existing unrelated nightly-clippy warnings fixed under scope:
fabro-sandbox sandbox_spec.rs (unused_imports, unused_async),
reconnect.rs (unused_variables, unused_async).
Verified: cargo +nightly-2026-04-14 clippy --workspace --all-targets
-- -D warnings passes; fmt clean; 4129/4131 tests pass (two known
flakes under parallel nextest load, both pass individually and are
unrelated to this change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All 13 units shipped. Follow-ups noted inline: globset-based denylist
extraction and Virtualizer wrapping for very large runs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes Units 11 and 12 of the Run Files Changed plan:
- Empty-state taxonomy: distinct copy for total_changed==0 vs no
recoverable diff
- LoadingSkeleton on initial loader navigation (shimmer respects
prefers-reduced-motion via motion-safe:animate-pulse)
- ErrorBoundary export handling 401/403/503/429 and generic 5xx
- Refresh button + Toolbar with freshness indicator; relative
timestamps tick every 10s
- SSE subscription to /runs/{id}/attach with a 500ms debounce that
revalidates on checkpoint.completed, run.completed, run.failed
- After a revalidation completes, focus returns to the Refresh button
- j/k keyboard navigation over file rows, ignoring key presses while
a text field is focused
- md (768 px) breakpoint collapses split to unified without writing
any persisted preference
- #file=<encoded-path> deep link scrolls + focuses the matching row
on mount; absent file surfaces a 5s toast; patch-only mode shows
a toast explaining the limitation
- Touch targets on the Refresh button meet WCAG 2.5.5 AAA (44x44)
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
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>
Light mode had the page at pure #ffffff with panels at #f8fafc — so
panels read darker than the page, the opposite of dark mode's
hierarchy and a big source of "blinding white" fatigue.
- page tinted to #f3f6fa (cool off-white, matching the brand's navy
palette) so it no longer glows
- panel set to #ffffff so cards, the nav, and auth panels pop
- panel-alt (#e9eef5) sits between them for recessed wells
- overlay / line colors shifted from pure black rgba to the navy tint
so the whole system reads coherent
Dark-mode tokens unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures what landed in this session (Units 1-10, 13) vs what's
deferred (Units 11-12) so follow-up work can pick up from a clean
baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites apps/fabro-web/app/routes/run-files.tsx to consume the real
PaginatedRunFileList response and removes the fallbackFiles fixture
and the Steer subsystem. The new component:
- Loads via apiJsonOrNull, so a 404/501 (dev without the route)
renders the empty state instead of the root error boundary
- Branches on meta.degraded + meta.patch to render PatchDiff with a
DegradedBanner whose copy reflects degraded_reason
- Renders per-entry placeholders for sensitive, binary, symlink/
submodule, and truncated files with the priority order
sensitive > binary > symlink/submodule > truncated -- security
flags never get hidden behind a lesser placeholder
- Renders one MultiFileDiff per regular entry
- Uses role="region" + aria-label on each file row
Also unhides the Files Changed tab in run-detail.tsx by flipping
broken: true -> false. Adds missing final_patch: None to the runner
RunFailed test fixtures to match the lifecycle change from Unit 2.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The page wrapped its body in mx-auto max-w-4xl, which centered the
description and JSON inside the shell's max-w-5xl column. The
shell's "Settings" header used the outer 5xl bounds, so everything
below it shifted right. Let the page inherit the shell's width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously Settings was reachable only via direct URL or logout menu.
Add it to the nav (visible in both demo and real modes) and drop the
now-redundant in-page title so the shell header supplies the heading
instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 of the std::fs lint initiative. Refactors blocking std::fs entry
points that ran inside async contexts. Caller chains either converted to
async (using tokio::fs) or wrapped in tokio::task::spawn_blocking where
sync callers were already natural (Command builders, flock semantics).
HIGH (per-request async hot paths):
- fabro-sandbox local.rs: wrap recursive std::fs::read_dir traversal in
spawn_blocking. Fixes /api/runs/{id}/files stalling workers under
concurrent or deep listings.
- fabro-server static_files.rs: convert serve/serve_install/serve_with_mode
and the static-asset load chain to async; use tokio::fs::read for the
debug-only disk fallback. Cascades through install.rs build_install_router
(now async) and ~17 test call sites.
LOW (async but not per-request):
- fabro-workflow artifact.rs: sync_artifacts_to_env, offload_large_values
→ tokio::fs::read_to_string.
- fabro-workflow artifact_snapshot.rs: compute_artifact_info → async +
tokio::fs::read.
- fabro-server ip_allowlist.rs: load_cache and store_cache → async +
tokio::fs::{read,write,create_dir_all}.
- fabro-server server.rs: wrap worker_command invocation in spawn_blocking
at the async boundary in execute_run_subprocess; keep the sync
worker_command + current_server_target signatures intact.
- fabro-cli server/start.rs: wrap the OpenOptions::open call in
acquire_lock in spawn_blocking; file-lock semantics require a real
std::fs::File, and the flock polling loop stays async with time::sleep.
Deferred:
- fabro-llm load_file_as_base64 (file:// attachment loader): 7 call sites
across 4 providers, each inside sync translators. Left for Phase 3
annotation with a FOLLOW-UP marker; file:// URLs are rare in practice.
Verified: workspace builds, 4131 tests pass, 182 skipped. The lint that
enforces this discipline lands in the next commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps @pierre/diffs from 1.0.11 to 1.1.15 to pick up the Virtualizer
component and renderHeaderPrefix/renderCustomHeader hooks the Run
Files tab relies on for large-diff performance. 1.0 -> 1.1 merged
MouseEventManager/LineSelectionManager into InteractionManager but
the public React components (MultiFileDiff, PatchDiff, FileDiff,
File) keep their existing shape, so no consumer changes are needed
yet -- Unit 10 exercises the new features.
Pins an exact version (1.1.15) rather than a caret range so bun
doesn't resolve up to 1.1.16, which was published today and would
trip the "no packages younger than 24 h" rule in the user-global
policy.
The redundant apps/fabro-web/bun.lock is removed; bun workspaces
resolve against the root bun.lock and the per-app lockfile was
drifting from it. Embedded SPA bundle (lib/crates/fabro-spa/assets/)
is refreshed to match the new build output.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CLI's store-run lookup now passes `include_archived=true` so resolve
and bulk operations (archive, unarchive, rm, inspect, rewind) can still find
archived runs. The web UI's hand-maintained `RunStatus` union and display
map learn `archived` with a gray style so archived runs render correctly.
Default `fabro ps` continues to hide archived via `is_active()`; `-a`
shows everything including archived.
Extends the Fabro logging strategy with an explicit prohibited-fields
table covering the Run Files Changed endpoint's sensitive surface:
diff_contents, per-changed-file file_path values, raw git_stderr,
and credential-ish strings. Each entry pairs the prohibition with a
concrete cardinality-bounded alternative, so future handlers have a
precedent to follow rather than rediscovering the rule.
The Run Files handler (Unit 5) already emits exactly the allowlisted
field set (run_id, file_count, bytes_total, duration_ms, truncated,
binary_count, sensitive_count, symlink_count, submodule_count); this
change makes the policy enforceable for other endpoints.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the not_implemented placeholder in the demo router with a
demo::list_run_files_stub that returns a small illustrative
three-file diff (modified, added, renamed) matching the real handler's
PaginatedRunFileList wire shape. The stub ignores run_id and state so
demo mode and real mode cannot cross-contaminate (R34).
Unit 10 (frontend rendering paths) will remove the now-obsolete
client-side fallbackFiles fixture when it rewrites run-files.tsx.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new top-level commands mirror `fabro rm`'s bulk-by-ID shape: positional
run identifiers, per-ID success/error aggregation, and a final non-zero exit
if any item failed. Calls the new server endpoints from Unit 5. Shared bulk
loop covers both directions and emits structured JSON with an `archived` or
`unarchived` list alongside `errors`. Top-level help snapshot updated.
Extends the Run Files handler with the patch-only fallback branch.
When the sandbox is unreachable (reconnect failed, provider not
compiled in, or the base revision has been garbage-collected), the
response now:
- Reads RunProjection.final_patch (captured at run end by Unit 2
for both Success/PartialSuccess and now Failed runs)
- Caps the patch at 5 MiB on a UTF-8 char boundary
- Filters denylisted file sections out via a regex-level `diff --git`
header scan (no full patch parser; the placeholder line kept so
clients still render the surrounding context)
- Picks the right degraded_reason: provider_unsupported for Docker-
provider runs this build can't reconnect to, sandbox_gone for
terminal runs, sandbox_unreachable for still-running ones
- Populates meta.to_sha from conclusion.final_git_commit_sha and
meta.total_changed from a `diff --git` header count
When final_patch is absent (old Failed runs, projection write
failures), returns the empty envelope that the UI maps to R4(c).
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the OpenAPI spec with POST /api/v1/runs/{id}/archive and /unarchive
operations, adds `archived` to the RunStatus enum, and adds an
`include_archived` query param to listRuns. Regenerates the progenitor-built
Rust types and the typescript-axios client. Implements `archive_run` and
`unarchive_run` handlers via `operations::archive/unarchive`, and extends
`list_runs` to filter archived runs unless opted in. Archived runs continue
to map through `api_status_from_workflow` and bypass the board column.
Implements the sandbox branch of the Run Files Changed endpoint. When
a run has a reachable sandbox, the handler:
- Parses the run_id and authenticates via AuthenticatedService
- Rejects any non-default from_sha/to_sha (v1 reserves them)
- Validates SHA format with a 7-40 hex regex before use
- Returns 404 for both missing-run and unauthorized access so
run-ID enumeration is not possible (IDOR-safe)
- Reconnects to the sandbox via a new try_reconnect_run_sandbox that
returns Ok(None) for the reconnect-failed case (Unit 6 will insert
the final_patch fallback there instead of today's empty envelope)
- Enumerates changes via list_changed_files_raw + list_binary_paths,
batched blob fetching via stream_blob_metadata / stream_blobs
- Applies an inline sensitive-path denylist first (Unit 8 extracts),
then a 200-file count cap, per-file 256 KiB cap, and 5 MiB
aggregate cap - truncated entries carry an explicit
truncation_reason
- Builds a single tracing::info! span at response end with only the
allowlisted fields (run_id, file_count, bytes_total, duration_ms,
truncated, binary_count, sensitive_count, symlink_count,
submodule_count) -- no paths, contents, or git stderr
All calls go through the Unit 4 coalescing primitive, so concurrent
viewers of the same run share one materialization.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
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>
- run card: solid bg-panel at rest instead of bg-panel/80 (opacity
shift on hover was backwards)
- column header: mb-3 to match inter-card gap
- lifecycle tag: 10px → 11px (below readable threshold for uppercase)
- additions/deletions: tabular-nums so large counts don't jitter
- view toggle: add a bg-overlay active state so the active view is
not purely a hue shift; wrap in role="group" with aria-pressed on
each button
- search + repo select: add aria-label and name attributes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- drop the semi-transparent bg-panel/50 on the top nav so the page
background stops bleeding through
- simplify the header separator to after:border-b with a single
bottom inset
- raise the page title from text-lg/6 to text-xl for room to breathe
- add isolate to the root so Headless UI portals don't fight the
header's stacking context
Leaves the demo-mode beaker toggle visible in real mode as a
follow-up; gating it cleanly needs a new feature flag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an `archived → unarchive first` guard to every mutation entry point
that could otherwise hit an opaque 409 or confusing 404 on an archived run:
start, cancel, pause, unpause, submit_answer, and append_run_event server
handlers; the resume operation; and the rewind CLI command. append_run_event
also rejects client-injected `run.archived` and `run.unarchived` bodies so
lifecycle transitions cannot bypass the operations layer. Worker-emitted
run.completed / run.failed events still flow through as before. Fork reads
from the source's metadata branch only — no source mutation — so no guard
is needed there.
Both pages previously dumped raw JSON with no context. Add a heading
and a one-line description so a user landing from the nav understands
what they're looking at and how to edit it.
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>
Move wait_for_path, wait_for_log_line, stop_pid, server_log_files, and
isolated_storage_dir out of the three integration test files that duplicated
them and into fabro-test's public surface next to apply_test_isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- drop the unwired "Open PR" button; restore when RunPullRequest gains a url field
- drop the Terminal <Menu> block; both entries were non-functional
and the Web Terminal link pointed at a hardcoded Daytona dev URL
- drop the "Files Changed" tab; its loader hit a non-existent path and
the tab was already hidden behind a broken flag
- promote the remaining Preview button to primary teal styling so the
action bar has a clear primary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the concurrency primitive the upcoming GET /runs/{id}/files
handler needs so concurrent viewers of the same run share one
sandbox-git materialization (different runs still materialize in
parallel).
Design notes:
- Materialization runs on a detached tokio::spawn so an abandoned
caller cannot leave orphan git subprocesses in the sandbox
- tokio::sync::watch is used (not broadcast) so late subscribers that
arrive after the value is sent still see it via the cached `borrow`
- AssertUnwindSafe().catch_unwind() turns materializer panics into
500 ApiErrors for every concurrent caller; a subsequent request on
the same run_id then triggers a fresh materialization (no poisoning)
- ApiError::Clone is derived so the shared Arc<Result<T, ApiError>>
can fan out cheap copies
The FilesInFlight registry is now a field on AppState; Unit 5 will
consume it from the real handler.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Merge prepare_foreground_server_bootstrap and prepare_server_sink_bootstrap
into one prepare_server_bootstrap(config, storage, foreground).
- Drop three one-line settings_layer_* passthroughs from user_config; callers
now use load_settings_with_{storage_dir,config_and_storage_dir} directly.
- Swap underscore-prefixed lock field for #[expect(dead_code, reason=…)] to
document RAII intent explicitly.
- Remove two narrate-what-it-does comments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Centralizes the terminal-only precondition, idempotent behavior, and
event emission for archiving and unarchiving runs. Both operations
return typed outcomes distinguishing a real transition from an idempotent
no-op. `unarchive` reads prior_status from the projection (populated by
the RunArchived apply arm) rather than scanning the event log, so replay
stays pure append-and-apply.
Adds sandbox-side helpers the upcoming GET /runs/{id}/files handler
needs to produce structured diff entries without a full unified patch:
- list_changed_files_raw: git diff --raw -z --find-renames=50%,
returns RawDiffEntry variants (Added/Modified/Deleted/Renamed/
Symlink/Submodule) with SHA-addressed blob references; paths are
metadata only and never re-interpolated into shell
- list_binary_paths: git diff --numstat text/binary classifier so
binary blobs are never piped through cat-file
- stream_blob_metadata / stream_blobs: batched git cat-file
--batch-check / --batch driven by printf into stdin, avoiding
per-file RPC storms for 200-file runs
- DiffError discriminates Transient (timeout, process kill) from
Permanent (bad/invalid revision, unknown object) so the server can
surface 503 vs fall through to the patch-only fallback
All new invocations use a hardened git prefix (core.hooksPath=/dev/null,
protocol.file.allow=never, core.fsmonitor=false) plus a small env
hardening map (GIT_TERMINAL_PROMPT=0, GIT_EXTERNAL_DIFF cleared) and a
10 s timeout per R32.
Refs plan docs/plans/2026-04-19-002-feat-run-files-changed-tab-plan.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route server-owned logs to <storage>/logs/server.log from the start of
tracing, remove legacy home/config ownership paths, and fail fast when
a running legacy daemon is detected instead of silently proceeding.
This also adds the missing sink-resolution, truncate/append,
concurrency, legacy-config, and uninstall regression coverage for the
home/storage cleanup plan.