Commit graph

536 commits

Author SHA1 Message Date
Bryan Helmkamp
4bc278de39
feat(web): order Models above Integrations in settings nav
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:28:39 -04:00
Bryan Helmkamp
732d57483b
feat(web): round units on the resources settings page
The resources page mixed precise and rounded values (8.2%, 5.0s,
51.8 GiB). Round everything to whole units for at-a-glance reading.

Add an optional fractionDigits param to formatBytesAsMemory and
formatDurationMs (default 1, so sandbox memory limits and turn
durations keep their decimals) and have settings-resources opt into
fractionDigits 0. formatPercent now uses Math.round.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:28:39 -04:00
Bryan Helmkamp
37d6b3dbcd
fix(server): count whole storage tree in Fabro-managed bytes
build_disk_usage_response only summed scratch/ run dirs and logs/*.log,
omitting objects/ (SlateDB + artifacts), sessions/, and vaults/ — a ~30x
undercount of "Fabro managed" storage on the resources page.

Measure the whole storage_dir tree for total_size_bytes so it can't drift
as new subdirectories are added. Reclaimable stays a curated estimate that
matches what `fabro system prune` actually frees. A residual "other"
summary row keeps `fabro system df` totals consistent and surfaces as a
"Database & artifacts" table row.

Also add a KiB tier to formatBytesAsMemory so small storage values render
human-readably instead of raw byte counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 11:28:39 -04:00
Bryan Helmkamp
9201ef9fe6
feat(web): add Ask Fabro assistant page (#334)
Some checks are pending
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary

Adds `/ask-fabro`, a prototype route that brings the right-docked "Ask
Fabro" assistant into the real web app. It graduates the sidebar design
from the `docs/superpowers/prototypes/2026-05-16-chats-new` prototype: a
placeholder workspace page with an "Ask Fabro" trigger that toggles an
animated 420px docked panel. The panel streams scripted, **fake** AI
replies through assistant-ui — no real model calls — matching the
behavior of the existing `/chats` prototype.

## What changed

- **`routes/ask-fabro.tsx`** — the route. A placeholder "Runs" workspace
(stat cards + recent-runs list) whose only job is to host the trigger
button, plus the docked sidebar. Uses `handle = { hideHeader,
fullHeight, wide }` and the edge-bleed wrapper copied from the shipping
`chats-layout`.
- **`components/chats/ask-fabro-sidebar.tsx`** — animated-width 420px
assistant panel rendering assistant-ui's `<Thread>`.
- **`components/chats/sidebar-composer.tsx`** — compact single-line
composer pill for the narrow column.
- **`app.css`** — the `.ask-fabro-sidebar` CSS block (narrow-column
overrides, layered into `assistant-ui` to beat its unlayered defaults),
ported verbatim from the prototype.
- **`router.tsx`** — registers the route under the AppShell.

The components and CSS are faithful, near-verbatim ports of the
prototype, which was carefully constructed. The runtime is fully reused
— `chats-runtime`, `chats-script`, `chats-types`, and `tool-fallback`
already graduated with `/chats`, so this PR adds no new chat plumbing.

## Decisions

- **Route-local state, not context.** The prototype used an app-level
`AskFabroContext` so the sidebar could mount above the top nav. This
route is self-contained, so a plain `useState` passed as props is
simpler and equivalent.
- **Sidebar sits below the top nav** (within the route), rather than
spanning the full window like the prototype. Intentional — keeps the
route self-contained.
- **Not added to the nav.** Reachable directly at `/ask-fabro`; it is
not `demoOnly`, so it renders regardless of demo mode.

## Verification

- `bun run typecheck`, `bun test` (403 pass), and `bun run build` all
clean.
- Rendered side-by-side against the prototype's `/sample`: empty state
and active thread (user bubble + streamed markdown assistant reply)
match.

---

[![Compound Engineering
v2.60.0](https://img.shields.io/badge/Compound_Engineering-v2.60.0-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via
[Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:11:54 -04:00
Bryan Helmkamp
412f57f5ae
feat(web): add secrets management settings page (#327)
## What

Adds a **Settings → Secrets** page so secrets can be managed from the
browser, backed by the existing secrets HTTP API and generated TS
client.

- **`/settings/secrets`** — lists stored secrets (name, type badge,
description, last-updated) and deletes them through the shared confirm
dialog.
- **`/settings/secrets/new`** — the create form for **token** and
**file** secrets. OAuth secrets still list and delete here, but are
created by provider sign-in flows, not typed by hand (matching the CLI's
`secret set`).
- The **Secrets** entry is added to the settings sidebar nav.

## How

- `secretsApi` wired into `api-client.ts`; `useSecrets()` SWR hook +
`secrets` query key.
- New sibling routes `secrets` and `secrets/new` under `settings` (same
pattern as `runs` / `runs/:id`).
- The settings layout gains optional **handle-driven** `description` and
`headerAction`. When a page declares them, the layout renders title +
subheading + a vertically-centered header action button as one unified
header. Other settings pages are unaffected — they fall back to the
existing title-only header.

## Notes

- Values are write-only: the API never returns secret values, and the UI
never displays them.
- Reuses existing primitives throughout (`Panel`, `Badge`,
`ConfirmDialog`, `useToast`, button/input classes) — no new shared
components.
- Verified: `bun run typecheck` and `bun run build` pass; routes serve
200.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:50:09 -04:00
Bryan Helmkamp
40ed64c1c2
Add system resources settings page (#328)
## Summary

Adds server-visible resource reporting and a compact Resources settings
tab for CPU, memory, and the filesystem that contains Fabro storage.

## Changes

- Adds `GET /api/v1/system/resources` backed by `sysinfo`, including CPU
sampling, cgroup-aware memory reporting, storage filesystem matching,
and Fabro-managed disk byte totals.
- Extends the OpenAPI contract and regenerates the Rust and TypeScript
API clients.
- Adds a deterministic demo-mode resources route.
- Adds `/settings/resources` with 5 second polling and panels for
overview, CPU, memory, disk, and notes.
- Adds server integration/unit coverage and web route/render coverage.

## Screenshot

![Resources settings
page](https://raw.githubusercontent.com/fabro-sh/fabro/feature/system-resources-settings/docs/public/images/web/settings-resources.jpg)

## Verification

- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cargo nextest run -p fabro-server --features test-support --test it
api::system`
- `cargo test -p fabro-server resource_sampler::tests`
- `cd apps/fabro-web && bun test`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun run build`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound%20Engineering-Codex-6f42c1)](https://github.com/compound-engineering)

🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 17:22:53 -04:00
Jess Martin
5bfd115339
[codex] Add ACP steering support (#329)
## Summary
- Adds a backend-neutral live control abstraction so steering,
interrupt, and interrupt+steer no longer depend on API-only session
handles.
- Reworks ACP sessions into a live protocol loop that uses ACP
`session/prompt` for follow-up steers and ACP `session/cancel` for
interrupts without restarting the process.
- Registers ACP sessions as steerable, removes the stale non-steerable
server/UI/API path, preserves ACP projection metadata, and keeps
unsupported backends out of the steerability gate.

## Validation
- `LC_ALL=C cargo nextest run --workspace --no-fail-fast` (5,833 passed,
178 skipped)
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1
ASDF_BUN_VERSION=1.3.11 bun test` (396 passed)
- `cd apps/fabro-web && LC_ALL=C ASDF_NODEJS_VERSION=20.13.1
ASDF_BUN_VERSION=1.3.11 bun run typecheck`
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && LC_ALL=C ASDF_BUN_VERSION=1.3.11
bun run typecheck`

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-20 17:21:56 -04:00
Bryan Helmkamp
03d9083efa
feat(web): collapse unconfigured providers on models settings page
Configured LLM providers render directly; unconfigured ones move
behind a disclosure toggle. Starts expanded only when nothing is
configured so the panel is not near-empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:22:11 -04:00
Bryan Helmkamp
a594eaaf58
feat(web): show Slack integration status on settings page
Add a Communication panel showing Slack integration enabled state
and default channel, alongside the existing GitHub panel reframed
as Version Control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:22:11 -04:00
Bryan Helmkamp
ac9385a68e
refactor(web): simplify models settings page
Drop the default model, base URL, and provider slug badge from each
provider row. Rows now show the display name (slug fallback), model
count, and configuration status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 16:22:11 -04:00
Bryan Helmkamp
32015b2226
fix(graph): support dotted Fabro graph attributes (#324)
## Summary

Graph rendering now accepts documented Fabro dotted DOT attributes end
to end while keeping raw Graphviz calls behind `fabro-graphviz`. The new
`RenderableDot` boundary applies Fabro render styling and normalization
before raw SVG rendering, and both the CLI subprocess and server path
now route through that typed boundary instead of calling `graphviz_sys`
directly outside the graphviz crate.

The branch also adds a small curated DOT compatibility corpus covering
ACP agent attributes, human default choices, and subworkflow manager
attributes. Those fixtures are exercised by both render and validation
tests, and the run overview now shows graph render errors directly
instead of falling through to the empty graph state.

## Verification

- `cargo nextest run -p fabro-graphviz`
- `cargo nextest run -p fabro-validate`
- `cargo nextest run -p fabro-cli render_graph`
- `cargo nextest run -p fabro-server
render_graph_from_manifest_accepts_fabro_dotted_attributes`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-graphviz -p fabro-cli -p
fabro-server -p fabro-validate --all-targets -- -D warnings`
- `rg -n "graphviz_sys" lib/crates/fabro-cli lib/crates/fabro-server`
- `cd apps/fabro-web && bun test`
- `cd apps/fabro-web && bun run typecheck`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com>
2026-05-20 09:31:08 -04:00
Bryan Helmkamp
d17599898e
feat(web): show creator avatar on run "Created by" cell (#319)
## Summary

The run Overview tab's "Created by" cell rendered every user as a
colored circle with the first letter of their login. Reviewers and run
owners expected the same GitHub avatar shown on `/profile` and in the
top-right nav. The cell only had `login` to work with — the
`PrincipalUser` schema carried no avatar URL.

This threads an optional `avatar_url` through `UserPrincipal`
end-to-end: schema, server auth, and frontend. The avatar is captured at
action time from the request's auth context and persisted with the run's
`created_by` principal — a point-in-time snapshot, the same pattern as
audit logs and chat apps.

## What changed

- **`fabro-types`** — `UserPrincipal` gains `avatar_url: Option<String>`
with `#[serde(default, skip_serializing_if)]`, plus a
`Principal::user_with_avatar` constructor. The existing
`Principal::user` constructor is unchanged (sets `None`), so test
fixtures and CLI/replay call sites need no edits.
- **OpenAPI** — `PrincipalUser` gains an optional nullable `avatar_url`;
Rust (progenitor) and TypeScript clients regenerated.
- **`fabro-server`** — `auth_context_from_session` (cookie auth) and
`classify_user_token` (JWT auth) populate the principal's avatar from
the session/JWT, treating an empty string as `None`.
- **`fabro-web`** — the `run-summary-panel` "Created by" cell renders an
`<img>` when `avatar_url` is present, falling back to the initial circle
otherwise.

## Compatibility

The field is optional with serde defaults, so old persisted runs and
`RunEvent.actor` payloads deserialize unchanged — they show the
initial-circle fallback. No migration or backfill.

## Known gap

CLI-initiated runs (`fabro run ...`) still show the initial circle: the
CLI auth flow hardcodes an empty `avatar_url` in the JWT subject
(`cli_flow.rs:508`). Wiring the avatar through CLI login
(`~/.fabro/auth.json`, JWT claims, refresh-token chain) is a deliberate
follow-up. Web-initiated runs get the avatar today.

## Test plan

- `cargo nextest run --workspace` — 5,832 tests pass, including new
`principal.rs` and `principal_round_trip.rs` cases covering avatar
serialization and legacy-JSON (no-field) deserialization.
- `cd apps/fabro-web && bun test run-summary-panel` — 13 tests pass,
including a new case asserting the `<img>` renders with the avatar src.
- `bun run typecheck`, `cargo +nightly-2026-04-14 fmt --check --all`,
and `clippy --workspace --all-targets -- -D warnings` all clean.
- Manual: restart `fabro server`, create a run from the web UI, confirm
the real avatar renders on the Overview tab; confirm an older run falls
back to the initial circle.

---

[![Compound Engineering
v2.60.0](https://img.shields.io/badge/Compound_Engineering-v2.60.0-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with Claude Opus 4.7 (1M context, extended thinking) via
[Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:11:57 -04:00
Bryan Helmkamp
fbe8b50a16
feat(server): add GET /api/v1/providers and /settings/models page (#321)
## Summary

Operators had no UI surface to see which LLM providers their Fabro
server has configured — provider state was only inferable indirectly via
the per-model `configured` flag on `GET /api/v1/models`. This adds a
dedicated **Models** settings tab backed by a new providers endpoint.

- **`fabro_model::Provider`** — a public projection of the internal
`CatalogProvider` that *structurally* excludes credential-bearing fields
(`auth`, `extra_headers`, `billing_policy`, `agent_profile`). Reused by
the generated API client via progenitor `with_replacement`, mirroring
the existing `Model` pattern — no parallel API DTO.
- **`GET /api/v1/providers`** — lists catalog providers with effective
config and a `configured` status stamped per request from
`ready_llm_provider_ids()`. Sorted by the catalog's existing
`provider_order`. No write endpoints.
- **`/settings/models` web page** — new route + nav entry
(`CpuChipIcon`, between Integrations and Security) rendering each
provider with model count, default model, configured status, and a "Get
API key" link for unconfigured providers.

## Key decisions

- Provider sort: reuse catalog `provider_order` (priority desc, id asc)
— zero extra code.
- `adapter` is hidden in the UI row (noisy for first-party providers);
the OpenAPI `adapter` field is pinned to an enum matching the closed
`AdapterKind` type.
- `configured` reflects credential resolution **at the time of the
response**, not a frozen startup snapshot — doc/spec wording corrected
to match.

## Testing

- `fabro-model`: `From<&CatalogProvider>` + serde `skip_serializing_if`
unit tests.
- `fabro-api`: `Provider` type-identity + JSON-parity tests, including
the required/optional field split.
- `fabro-server`: handler tests for configured vs unconfigured
providers, exact `model_count`/`default_model` against catalog truth,
and credential-omission (asserts internal field names *and* the injected
credential value never reach the wire).
- OpenAPI route conformance test covers `GET /api/v1/providers`.
- `cargo build --workspace`, `fmt --check`, `clippy -D warnings` clean;
935 Rust tests pass; web `tsc` typecheck passes.
- Reviewed via a 10-persona `ce:review` (autofix) — no P0/P1 in shipped
code; 8 safe fixes applied.

Not done: manual UI screenshots — the `apps/fabro-web` build is blocked
in this environment by an unrelated missing `@assistant-ui/react`
dependency. Run `bun install` in `apps/fabro-web` to verify
`/settings/models` manually.

## Post-Deploy Monitoring & Validation

- **What to watch:** request logs for `GET /api/v1/providers` — expect
`200`s for authenticated users, `401` for unauthenticated. The handler
resolves LLM credentials per request via `ready_llm_provider_ids()` (the
same path the existing `list_models` handler already uses).
- **Healthy signals:** `/settings/models` renders the provider list;
`configured` matches each provider's actual credential state; no
credential strings appear in any response body or log line.
- **Failure signals / rollback trigger:** any provider object in the
response containing `auth`, `extra_headers`, or a raw key/token value →
roll back immediately (the projection type makes this structurally
impossible, but treat any occurrence as P0). 5xx spikes on the new
route.
- **Validation window / owner:** first 24h after deploy, owned by the
deploying engineer. Pre-existing note (not introduced here): credential
resolution can refresh OAuth tokens and write the vault as a side effect
of this read — shared with `list_models`; flagged for a future caching
pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 09:05:00 -04:00
Bryan Helmkamp
a772c0feb5
feat(web): split settings into General/Integrations/Security/Storage tabs
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
Replaces the single Settings overview with four focused tabs, plus
the existing Live Events tab below a sidebar divider. JSON view is
kept only on General and shows the full server settings document.
The Storage tab splits Storage Root, SlateDB, and Artifacts into
separate panels, with object store fields broken into one row per
field via a shared ObjectStoreRows helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 16:13:25 -04:00
Bryan Helmkamp
29b7cc0de0
feat(workflow): enforce strict api/acp backends (#307)
## Summary

This PR makes agent execution a strict two-backend contract: API-backed
stages use Fabro-owned model/provider auth, while ACP-backed stages
launch a user-supplied stdio process that owns its own auth and tools.
That removes the legacy CLI backend and prevents ACP execution from
accidentally resolving or forwarding provider credentials.

## Changes

- Replaces the old `api`/`cli`/`acp` backend model with `AgentBackend {
api, acp }`, with `backend=\"cli\"` rejected and migrated toward
explicit ACP process configuration.
- Splits ACP process configuration into `acp.command` for shell command
strings and `acp.config` for JSON stdio configs, while rejecting legacy
`acp_command`.
- Restricts ACP to `agent` nodes and rejects API-only attributes such as
`model`, `provider`, `reasoning_effort`, `max_tokens`, and `speed` on
ACP nodes.
- Deletes the workflow CLI runtime, CLI credential resolver surface, CLI
live smoke tests, and `agent.cli.*` event handling.
- Updates ACP events and projections to report process identity
(`command`, optional `config_name`) rather than provider/model metadata.
- Updates import/stylesheet propagation, CLI workflow smoke coverage,
server steering tests, and web model extraction for the new
event/backend contract.

## Validation

- `cargo check -p fabro-auth -p fabro-acp -p fabro-workflow -p fabro-cli
--all-targets`
- `cargo nextest run -p fabro-auth -p fabro-acp -p fabro-validate -p
fabro-store -p fabro-workflow --lib`
- `cargo nextest run -p fabro-acp`
- `cargo nextest run -p fabro-cli --test it
workflow::acp::acp_backend_workflow`
- `cargo nextest run -p fabro-workflow --test it
codergen_without_backend_simulated`
- `cargo nextest run -p fabro-workflow --test it
import_e2e_through_engine`
- `cargo nextest run -p fabro-workflow --test it stylesheet_application`
- `cargo nextest run -p fabro-server
steer_with_active_acp_stage_returns_non_steerable_conflict`
- `cargo nextest run -p fabro-server
active_acp_stage_marker_clears_on_terminal_paths`
- `cargo nextest run -p fabro-types
agent_backend_accepts_only_api_and_acp`
- `cd apps/fabro-web && bun test app/routes/run-stages.test.ts`
- `cd apps/fabro-web && bun run typecheck`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Peter Bell <4843+PeterBell@users.noreply.github.com>
2026-05-18 13:20:56 -04:00
Bryan Helmkamp
c6547f126a
fix(web): raise timeout for build tests that run a real vite build (#310)
## Summary
- The TypeScript CI on main started failing because two tests in
`apps/fabro-web/scripts/build.test.ts` shell out to a real production
Vite build and were hitting Bun's default 5000ms test timeout.
- When the test timed out, Bun killed the build subprocess (SIGTERM →
exit 143), reported as `build failed with code 143`.
- Raise the per-test timeout to 60s on the two build-running tests so CI
variance no longer kills the build.

Failing run:
https://github.com/fabro-sh/fabro/actions/runs/26042062252/job/76556114190

## Test plan
- [x] `cd apps/fabro-web && bun test scripts/build.test.ts` passes
locally
- [ ] TypeScript CI passes on the PR

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 12:52:18 -04:00
David Bock
6a86ced77c
fix(server): persist manifest metadata names (#302)
When Fabro creates a detached run through the server, the resulting run
metadata should still read like something a human can trust at a glance.
Before this change, those runs could persist with
`settings.project.name` and `settings.workflow.name` left `null` even
though Fabro already had enough local context to infer them. That made
`inspect` output look half-populated and made it harder to tell whether
the saved run state was complete.

This fixes that trust gap in the server-backed manifest flow.

## Summary
- backfill missing manifest-backed project and workflow names during
server run preparation
- prefer explicit `[workflow].name` from bundled `workflow.toml`, then
fall back to graph name or workflow slug
- cover both manifest preparation and persisted run-state behavior with
server tests

## Testing
- cargo test -p fabro-server
prepare_manifest_backfills_missing_project_and_workflow_names --
--nocapture
- cargo test -p fabro-server
prepare_manifest_preserves_explicit_project_and_workflow_names --
--nocapture
- cargo test -p fabro-server
create_run_persists_backfilled_project_and_workflow_names -- --nocapture

---------

Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-18 08:31:23 -04:00
Bryan Helmkamp
581ab41d28
feat(web): add Overview summary panel and promote PR to header pill (#299)
## Summary

Promotes the most useful per-run state into the Overview tab so users no
longer have to tab-hop to read the basics of a run.

- **Adds a horizontal summary panel** above the workflow graph (right
column only — does not span the stages sidebar) with five columns:
**Created by · Changes · Sandbox · Cost · Artifacts**. Quiet-uppercase
labels (`text-[10px] uppercase tracking-[0.08em] text-fg-muted`) over
regular-weight values. Skeleton loaders while queries are in flight; em
dash in muted color for missing/zero data.
- **Promotes the PR chip** out of the meta strip into a
`SECONDARY_BUTTON_CLASS`-style pill next to the Actions menu, visible on
every tab. Pill renders only when a PR exists.
- Lifts `formatBytesAsMemory`, `formatCpuCores`, `formatUsdMicros` to
`lib/format.ts` so the panel can reuse them.
- New `RunSummaryPanel` is split into a smart wrapper (owns the SWR
hooks) + a presentational `RunSummaryPanelView` (prop-driven) for clean
test seams.
- All 7 `Principal` kinds (user / agent / system / slack / webhook /
worker / anonymous) map to glyph + label; user kind uses login-initial
avatar.

## Screenshots

Captured against a real local Fabro server (`fabro server start`) on
demo runs — these only exercise the Created-by column (the other cells
display em dashes because the demo runs have no PR / diff / billing /
artifacts data). The em-dash states **are** the intended empty-state
design.

### Overview tab — full page

![Overview tab](https://files.catbox.moe/2idmv5.png)

### Header + tabs + summary panel close-up

![Header and panel](https://files.catbox.moe/bugbmy.png)

### Summary panel detail

![Summary panel](https://files.catbox.moe/4sbcl0.png)

> The PR pill (mint icon + `#number` next to Actions) is unverified
visually because no demo run on this server has an associated PR — but
the rendering path is the same `SECONDARY_BUTTON_CLASS` markup as the
Actions button and is conditioned on `run.pullRequestUrl && run.number
!= null`. See the [HTML
prototype](https://github.com/fabro-sh/fabro/blob/feat/run-overview-summary-panel/.context/run-overview-options.html)
for the locked design.

## Test plan

- [x] `cd apps/fabro-web && bun run typecheck` clean (only pre-existing
assistant-ui errors)
- [x] `bun test` — +12 new passes, no new failures (387 pass / 5 fail /
2 errors vs baseline 375 / 6 / 3)
- [x] Manual: load `/runs/<id>` against a real server, confirm panel +
em dashes render correctly
- [ ] Manual on a run **with** a PR: verify the pill appears next to
Actions and opens the PR in a new tab
- [ ] Manual on a run **with** rich data (diff / billing / sandbox
resources / artifacts): verify each column populates correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:52:48 -04:00
Bryan Helmkamp
d1fd6d3abf
fix(web): widen Children tab and hide zero count badge
The Children sub-tab was missing `wide: true` on its route handle, so
the Run detail nav narrowed to max-w-5xl only on this tab. Hide the
count badge when the value is zero to reduce visual noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:18:20 -04:00
Bryan Helmkamp
ac32963538
feat(web): add Children tab to Run detail page (#294)
## Summary

- Surfaces parent/child run relationships in the web UI as a new
**Children** tab between Files Changed and Sandbox on `/runs/:id`.
- Backend exposes a new `children_count` field on the `Run` summary,
computed on read from the existing
`RunProjectionCacheState.children_by_parent` index — accurate without an
extra query.
- Frontend reuses the compact-table `RunRow` from `/runs` (now exported)
so the children list matches the existing list-view at a glance.
- Tab always shows, with a zero state when there are no children.
Refresh button (icon-only, matching the Files Changed pattern)
re-fetches both the list and the parent detail so the count badge
updates with the list.

## Screenshots

Captured live from the running fabro server.

**Populated — `Children · 2` tab active, two succeeded child rows:**

![Children tab
populated](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/populated.png)

**Zero state — visiting a run that has no children:**

![Children tab zero
state](https://raw.githubusercontent.com/fabro-sh/fabro/feature/run-children-tab/.github/assets/children-tab/zero-state.png)

## API verification

```sh
# parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP5DJJ4EV6T7QSB081Z1N \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "parent_id": null,
  "children_count": 2
}

# child
$ curl -s -H "Authorization: Bearer $TOKEN" \
    http://127.0.0.1:32276/api/v1/runs/01KRTKP7VAS2J2AG73GQSAKF4G \
    | jq '{id, parent_id, children_count}'
{
  "id": "01KRTKP7VAS2J2AG73GQSAKF4G",
  "parent_id": "01KRTKP5DJJ4EV6T7QSB081Z1N",
  "children_count": 0
}

# list-by-parent
$ curl -s -H "Authorization: Bearer $TOKEN" \
    "http://127.0.0.1:32276/api/v1/runs?parent_id=01KRTKP5DJJ4EV6T7QSB081Z1N" \
    | jq '{count: (.data | length), has_more: .meta.has_more}'
{ "count": 2, "has_more": false }
```

## What's in each commit

| Commit | What |
| --- | --- |
| `2f5f4296` | `chore(api-client)`: regenerate TS client from current
OpenAPI spec — catches up drift from #292's source-aware diagnostics and
the session/turn shape updates that hadn't been re-run yet. Pure
generator output. |
| `ba16d2b3` | `feat(web)`: the actual Children tab feature. Backend
`children_count` field + cache wiring, new `useChildRuns` SWR hook,
exported `RunRow`/`RUNS_LIST_GRID_TEMPLATE` from `runs.tsx`, new
`run-children.tsx` route, `Run.children_count` on the generated TS type.
|
| `de0c32c9` | `docs`: live UI screenshots for this PR. Safe to revert
before merge if reviewers prefer a screenshot-free repo. |

## Reproducing the screenshots

1. `cargo build -p fabro-cli && ./target/debug/fabro server start`
2. `cd apps/fabro-web && bun run build`
3. ```sh
PARENT=$(./target/debug/fabro run hello --dry-run --detach --sandbox
local --json | jq -r .run_id)
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
./target/debug/fabro run hello --dry-run --detach --sandbox local
--parent "$PARENT"
   ```
4. Open `http://127.0.0.1:<port>/runs/$PARENT/children` (populated) and
a child's children tab (zero state).

## Test plan

- [x] `cargo nextest run -p fabro-store -p fabro-types -p fabro-api -p
fabro-server -p fabro-mcp-server` — 900+ tests pass, including new
`run_summary_includes_children_count` in `fabro-store`
- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `cd apps/fabro-web && bun test` — 383/383 pass
- [x] OpenAPI ↔ Rust parity (the `fabro-api` `run_summary_round_trip`
test covers the new field both directions)
- [x] Manual API verification via curl (above)
- [x] Live UI verification (screenshots above)

## Out of scope (v1)

- Real-time SSE updates of the children list (refresh button covers
this).
- Multi-page pagination UI (shows first page with a "more exist" footer
when `has_more`).
- Parent breadcrumb on the child run page (separate small change).
- Tree/nesting view (flat list only).
- Empty-state CTA.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:21:40 -04:00
Bryan Helmkamp
7ac15b28f0
feat(fabro-web): port /chats/new + /chats/:id from prototype (#289)
## Summary

Ports the validated `/chats/new` and `/chats/:id` chat surface from
`docs/superpowers/prototypes/2026-05-16-chats-new/` into
`apps/fabro-web`. Client-side scripted prototype mounted inside the
existing `AppShell`; replaces `/start` as the planned new "kick off
agent work" entry point (but does not delete `/start` in this phase).

- New routes: `/chats/new` (empty-state composer) and `/chats/:chatId`
(active conversation with assistant-ui's `<Thread>`, scripted streaming
replies, markdown + tool-call rendering).
- Drives `@assistant-ui/react` + `@assistant-ui/react-ui` via
`useLocalRuntime` and a custom `ChatModelAdapter` that cycles a 6-entry
scripted reply bank.
- Tailwind v4 cascade fix: assistant-ui CSS is now imported via `@layer
assistant-ui` so v4 utilities cascade above the package's unlayered
scoped preflight. Includes a discovered Bun-specific tweak — see Notable
Deviations below.
- StrictMode-safe first-message handoff: store seeds the user message
into `seedMessages` with a `pendingResponse: true` flag, and
`chats-detail` triggers a single `runtime.thread.startRun({ parentId:
null })` then consumes the flag. Avoids the prototype's
autorespond-lost-stream race under React 19 StrictMode.

The Ask-Fabro right sidebar (also in the prototype) is **out of scope**
for this PR.

Companion spec:
[`docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md`](../tree/chats-new-port/docs/superpowers/specs/2026-05-16-chats-new-prototype-design.md)
Implementation plan:
[`docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md`](../tree/chats-new-port/docs/superpowers/plans/2026-05-16-chats-new-fabro-web-port.md)

## Screenshots

Captured from a local debug `fabro server` running this branch's binary,
signed in via GitHub.

### `/chats/new` (empty state)

![chats-new empty
state](https://github.com/fabro-sh/fabro/raw/chats-new-port/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-new-v4.png)

### `/chats/:chatId` (active conversation)

![chats-detail active
chat](https://github.com/fabro-sh/fabro/raw/chats-new-port/docs/superpowers/prototypes/2026-05-16-chats-new/screenshots/chats-detail-v4.png)

## Files

**New** (under `apps/fabro-web/`):
- `app/lib/chats-types.ts` — `Chat` wrapper + `ChatContentPart`
discriminated union over the API client's `CompletionContentPart`
- `app/lib/chats-script.ts` — 6-entry scripted reply bank
(`CompletionMessage[]`)
- `app/lib/chats-store.tsx` — Context + `useReducer` for chat metadata,
`pendingResponse` flag, scriptIndex
- `app/lib/chats-runtime.ts` — `createScriptedAdapter` +
`toThreadMessages` boundary converter
- `app/lib/test-utils.tsx` — minimal `renderHook` shim (lifts the
duplicated `IS_REACT_ACT_ENVIRONMENT` + dep-warning silencing pattern
out of `install-app.test.tsx`)
-
`app/components/chats/{tool-fallback,composer-chips,custom-composer}.tsx`
- `app/routes/{chats-layout,chats-new,chats-detail}.tsx`
- Tests: `chats-store.test.tsx` (5), `chats-runtime.test.ts` (4),
`chats-router.test.tsx` (3)

**Modified:**
- `package.json` — adds `@assistant-ui/{react,react-ui,react-markdown}`
(pinned exactly to versions verified in the prototype)
- `app/app.css` — `@layer` declaration + assistant-ui CSS imports into
`layer(assistant-ui)` + `.fabro-chat` `--aui-*` variable overrides
mapping to the Fabro palette
- `app/root.tsx` — removed `import "./app.css"` (see Notable Deviations)
- `app/router.tsx` — wires the chats routes under the AppShell tree

## Notable deviations from the plan

Two intentional deviations, both explained in their commit bodies:

1. **`apps/fabro-web/app/root.tsx` no longer imports `./app.css`.**
Bun's CSS bundler (used by `Bun.build` on `entry.tsx`) rejects
spec-valid `@layer name, name;` ordering between `@import` rules, even
though Tailwind's CLI accepts it. The CSS is built standalone by the
Tailwind CLI step in `scripts/build.ts` and linked from
`index.template.html`, so dropping the JS-side import bypasses Bun's
parser without any runtime change. A safety-net comment at the top of
`app.css` warns future engineers against re-adding the import. Commit:
`c37690be9`.
2. **`!` non-null assertions removed** in two places where the verbatim
prototype copy violated the global CLAUDE.md rule banning `!` in
production code: `chats-script.ts` now uses a typed `FALLBACK_REPLY` and
`??` coalescing; `composer-chips.tsx` lifts the first option of each
chip into a `DEFAULT_*` constant. `chats-runtime.test.ts`'s `for await`
drain loops were also replaced with `Array.fromAsync(...)` per the
no-loops-in-tests rule. Commits: `ace6ac6d4`, `652ad97af`.

## Test plan

- [x] `cd apps/fabro-web && bun run typecheck` — clean
- [x] `bun test` — 383 pass / 0 fail (12 new tests for chats)
- [x] `cd apps/fabro-web && bun run build` — succeeds; assistant-ui CSS
bundled into `dist/assets/app.css`
- [x] **Manual browser smoke test** — debug `fabro` binary running this
branch served `/chats/new` and `/chats/seed_email` correctly inside the
real AppShell with GitHub-OAuth auth (screenshots above).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:10:16 -04:00
Bryan Helmkamp
4f5e3b78f8
refactor: remove compatibility shims (#281)
## Summary
Simplifies the greenfield PR/run schema surface by collapsing alias-only
type shims and removing legacy compatibility paths that kept old wire
shapes and workflow names alive.

## Changes
- Use canonical `Run`, `PullRequestLink`, `PullRequestResponse`,
`BoardColumn`, `WorkflowSettings`, SWR `Key`, and `SteerRunRequest`
names directly across Rust and web code.
- Remove legacy PR/event deserialization compatibility for old PR
records and command output fields, with tests updated to reject stale
wire shapes.
- Drop obsolete workflow aliases for `agent_loop`, `one_shot`,
`codergen_mode`, and `stack.child_dotfile`, then update docs and tests
to the current names.

## Verification
- `git diff --check`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo nextest run -p fabro-types -p fabro-api -p fabro-client -p
fabro-store -p fabro-server -p fabro-workflow -p fabro-cli`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:29:25 -04:00
Bryan Helmkamp
d09e6cde33
feat(pr): support GitHub pull request associations (#270)
## Summary

Adds event-sourced pull request association management for runs while
preserving Fabro-created PR creation. A run can now store a current
GitHub PR association, replace it by linking another GitHub PR URL, and
remove it through an unlink event.

## What Changed

- Added `pull_request.linked` and `pull_request.unlinked` events,
projection replay support, and optional PR metadata fields in shared
pull request records.
- Added API, server, and client support for `PUT
/runs/{id}/pull_request` and `DELETE /runs/{id}/pull_request`; linking
accepts GitHub PR URLs, infers owner/repo/number, and captures live
GitHub title and branch metadata when available.
- Added `fabro pr link` and `fabro pr unlink`, updated `fabro pr view`,
and kept create/merge/close behavior guarded to GitHub PRs with usable
coordinates.
- Updated web UI rendering and internal event docs so stored PR links
display cleanly when live GitHub details are unavailable.

## Testing

- `cargo +nightly-2026-04-14 fmt --check --all`
- `git diff --check`
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-types -p fabro-store -p fabro-server -p
fabro-cli`
- `bun run typecheck` in `lib/packages/fabro-api-client`
- `bun run typecheck` in `apps/fabro-web`
- `bun test` in `apps/fabro-web`

Refs https://github.com/fabro-sh/fabro/issues/235

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
2026-05-16 12:47:27 -04:00
Bryan Helmkamp
f790a47da4
feat(sandbox): surface provider links and network policy (#275)
## Summary
- Add provider dashboard URL reporting to `SandboxDetails`, including
Daytona dashboard links and a Sandbox tab provider link.
- Extend `SandboxDetails` with required provider-neutral public network
policy for egress and ingress allow/block rules.
- Populate local, Docker, and Daytona network policies from provider
details when Fabro can assert them, otherwise default to explicit
`unknown` policy.
- Update OpenAPI, Rust API replacements, generated TypeScript client
models, server/API tests, and the Sandbox tab `Network` panel.

## Notes
- This reports policy only; it does not probe live connectivity.
- The network model intentionally excludes ports, previews, IP
addresses, DNS, routes, Docker network IDs, and service discovery.
- Older persisted/API JSON still deserializes through the Rust serde
default for `network`.

## Verification
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-types -p fabro-api -p fabro-sandbox -p
fabro-server sandbox_details`
- `cargo test -p fabro-sandbox details --features docker,daytona`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cd apps/fabro-web && bun test app/routes/run-sandbox.test.tsx`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun run build`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `git diff --check`

## Post-Deploy Monitoring & Validation
- Log queries/search terms: `Failed to inspect Docker container`,
`Daytona sandbox is not initialized`, `missing runtime metadata`, `GET
/api/v1/runs/*/sandbox`, `Sandbox unavailable`.
- Metrics/dashboards to watch: API 5xx/error rate for `GET
/api/v1/runs/{id}/sandbox`, frontend error reporting for the Sandbox
tab, and provider reconnect/inspect failures.
- Expected healthy signals: Sandbox tab loads with Overview, Resources,
Network, Labels, and Timestamps; Daytona sandboxes show provider links;
local/ambiguous network policy shows `Unknown`; Docker `network_mode =
none` shows `Blocked`.
- Failure signals and rollback trigger: sandbox details deserialization
errors, missing `network` crashes, sustained sandbox endpoint 5xx
increase, or blank Sandbox tab after deploy. Roll back this PR or hide
the Network panel if API/client shape issues appear.
- Validation window and owner: first 24 hours after deploy, release
owner/on-call.

## Compound Engineering
- Implemented with OpenAI Codex CLI on GPT-5.
2026-05-16 10:16:18 -04:00
Bryan Helmkamp
32f100cbe7
feat(install): make LLM setup optional in web installer and CLI (#265)
Some checks failed
Rust / Clippy (push) Waiting to run
Rust / Format (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## Summary

Makes LLM setup explicitly skippable in both the web installer and
`fabro install`, without making omission accidental. A skipped LLM step
lets install complete with zero LLM credentials; later LLM-dependent
workflows keep using the existing provider-not-configured behavior.
`fabro doctor` is intentionally unchanged.

Plan: `docs/superpowers/plans/2026-05-14-optional-llm-install.md`

## Key changes

**Server + API**
- `PUT /install/llm` now accepts `{"providers":[]}` as "LLM step
completed, skipped" — the empty-list rejection is removed; per-provider
validation for non-empty lists is retained.
- OpenAPI: dropped `minItems: 1` from
`InstallLlmProvidersInput.providers`, updated schema descriptions so
empty = skipped and `llm: null` = incomplete. TypeScript client
regenerated.
- `/install/finish` still requires the LLM step to be present, but
tolerates zero credentials — it writes settings, runtime auth secrets,
and GitHub secrets normally and writes no LLM vault entries.

**Web installer**
- New "Skip LLM setup" secondary action on the LLM step (via a
`secondaryAction` prop on `StepPanel`) that records an empty provider
list and advances to GitHub.
- Review screen shows `LLM providers: Skipped` (step completed, empty)
vs `Not configured` (step never completed), via a new
`describeLlmSummary` helper.
- Continue with no API keys still shows the existing validation error —
skipping is only reachable through the explicit skip action.

**CLI**
- Interactive `fabro install` asks "Configure LLM providers now?"
(default yes) before provider selection; declining returns an empty
selection and continues to GitHub.
- Hidden non-interactive `--skip-llm` flag, mutually exclusive with
`--llm-provider` / `--llm-api-key-stdin` / `--llm-api-key-env` via clap
`conflicts_with_all`. Missing LLM flags are still validation errors
unless `--skip-llm` is present. Non-interactive usage text updated with
a skip example.

## Code review

Ran a 12-reviewer `ce:review` pass (correctness, testing,
maintainability, project-standards, agent-native, learnings, security,
api-contract, reliability, adversarial, cli-readiness,
kieran-typescript). No P0/P1 findings; agent-native parity PASS. Applied
fixes in `40a29c591`:
- Re-entrancy guard on `runStepSubmit` so a fast double-click on "Skip
LLM setup" can't fire two requests.
- `validate()` only suggests `--skip-llm` in the missing-provider error
when no credential flag is set (it conflicts with those flags).
- Added tests: all three `--skip-llm` conflict arms, the review screen's
"Not configured" branch, and the skip-button failure path.

One advisory finding left as report-only: an empty `PUT /install/llm`
overwrites previously-saved credentials if a user navigates Back and
clicks Skip — judged acceptable since the button is explicitly labeled
and clicking it is deliberate.

## Testing

- `cargo nextest run -p fabro-server -p fabro-cli -p fabro-install` —
1521 passed
- `cargo build -p fabro-api`, `cargo fmt --check`, `cargo clippy`
(changed crates) — clean
- `bun test` (install-app) — 14 passed; `bun run typecheck` — clean
- New coverage: server accepts empty providers + session shows `llm`
complete with `providers:[]`; finish with skipped LLM persists no LLM
vault credentials but keeps GitHub secrets; web skip button PUTs
`providers:[]` and navigates to GitHub; review renders Skipped / Not
configured; CLI `--skip-llm` requires `--non-interactive`, conflicts
with all credential flags, `validate()` succeeds with `--skip-llm`,
usage text documents `--skip-llm`.

Not added (out of plan scope): an automated test for the interactive
`InstallInputSource` skip branch — `InteractiveInstallInputSource` is
TTY-coupled and has no existing tests; the non-interactive `--skip-llm`
path is fully covered.

## Post-Deploy Monitoring & Validation

This change is install-time only; there is no continuous runtime impact.
Validate during the next install/release smoke:

- **Web installer:** run a fresh browser install, click "Skip LLM setup"
on the LLM step, confirm it advances to GitHub and the review screen
reads `LLM providers: Skipped`. Finish the install and confirm the
server restarts into normal mode with no LLM credentials in the vault
(`secrets.json` has no credential entries) and
GitHub/server/object-store/sandbox settings written normally.
- **CLI:** run `fabro install --non-interactive --skip-llm
--github-strategy token --github-username <user>` and confirm it
completes; run interactive `fabro install` and confirm declining
"Configure LLM providers now?" continues to GitHub.
- **Healthy signals:** install completes (web `/install/finish` → 202;
CLI exits 0), server boots in normal mode, `fabro doctor` runs and
reports no LLM providers configured (expected, unchanged behavior).
- **Failure signals / rollback trigger:** install fails to finish,
server fails to boot after a skipped install, or `/install/finish`
rejects a completed-but-empty LLM step. Rollback = revert this PR;
install behavior returns to requiring at least one LLM provider.
- **Validation window/owner:** next install smoke / release
verification, owned by whoever runs the release.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:34:39 -04:00
Bryan Helmkamp
1cfe9419f5
refactor(llm): simplify catalog cleanup paths (#261)
## Summary

This is a cleanup pass over the configurable LLM provider/catalog work
from issue #210. It addresses reuse, quality, and efficiency findings
from the phase 0-9 review without changing the public provider settings
contract.

Notable changes:

- skip LLM client initialization during run preflight when the workflow
has no LLM nodes
- make preflight provider checks use alias-aware `Client::has_provider`
- resolve `run.model.fallbacks` through the catalog instead of the old
empty-key bridge
- paginate `/models` before cloning returned rows
- share label parsing, provider default-adapter lookup, enum
expected-value formatting, and billing token formatting helpers
- use catalog provider display names for OpenAI-compatible agent
profiles
- align process-env configured-provider discovery with
`EnvCredentialSource`

## Verification

- `cargo check -p fabro-config -p fabro-model -p fabro-auth -p
fabro-agent -p fabro-workflow -p fabro-server -p fabro-cli`
- `cargo nextest run -p fabro-config parse_labels_keeps_key_value_pairs`
- `cargo nextest run -p fabro-workflow resolve_fallback_chain_resolves`
- `cargo nextest run -p fabro-auth configured_providers`
- `cargo nextest run -p fabro-server list_models`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy -p fabro-model -p fabro-auth -p
fabro-workflow -p fabro-server -p fabro-agent -p fabro-config -p
fabro-cli --all-targets -- -D warnings`
- `cd apps/fabro-web && bun test app/routes/run-billing.test.tsx`
- `cd apps/fabro-web && bun run typecheck`
- `git diff --check`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:35:42 -04:00
Bryan Helmkamp
a81eb09e78
feat(llm): add catalog controls and speed billing (#249)
## Summary

This PR advances the catalog-driven LLM work from fabro-sh/fabro#210 by
making the resolved model catalog the source of truth for provider
registration, request control validation, and billing identity. Runs now
preserve canonical provider/model/speed identity through pricing and API
responses instead of collapsing billing around provider API aliases or
model IDs alone.

## What Changed

- Register LLM provider adapters from the resolved catalog, including
custom OpenAI-compatible providers and their credential resolution
paths.
- Validate effective model request controls, including run-level
defaults and node overrides, before dispatching LLM requests.
- Add catalog-aware billing lookup that prices canonical `ModelRef`
values, uses base model costs for standard speed, applies per-speed cost
overrides, and returns an unknown estimate instead of silently billing
zero for unsupported combinations.
- Move Anthropic Opus fast-mode pricing into the built-in catalog for
`claude-opus-4-6` and `claude-opus-4-7`.
- Thread the injected catalog and effective speed controls through
workflow billing, including API-mode and CLI-mode handlers.
- Update billing APIs, server aggregation, generated clients, and the
web billing view to expose provider/model/speed billing identity and
keep standard and fast usage in separate rows.

## Notes for Review

Billing lookup intentionally uses canonical catalog model IDs. Provider
`api_id` substitution remains limited to provider request construction,
so aliases can be used on the wire without changing billing identity.
Event conversion paths that do not have catalog access now preserve
token counts with a null dollar estimate rather than falling back to the
bootstrap catalog.

## Verification

- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `ulimit -n 4096 && cargo nextest run -p fabro-model -p fabro-workflow
-p fabro-server -p fabro-api -p fabro-cli --no-fail-fast`
- `ulimit -n 4096 && cargo nextest run --workspace --no-fail-fast`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `git diff --check`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:12:16 -04:00
Bryan Helmkamp
a33d17c88d
feat(run): add managed branch controls (#243)
## Summary

Adds run-level controls for clone behavior, managed run branch
setup/pushes, and metadata branch writes/pushes so workflows can opt out
of Fabro-managed Git behavior without relying on provider-specific
`skip_clone` settings. This closes fabro-sh/fabro#240.

## What Changed

- Introduced `[run.clone]`, `[run.run_branch]`, and `[run.meta_branch]`
settings with defaults that preserve current behavior.
- Removed user-facing `skip_clone` from Docker/Daytona config while
mapping the new run-level clone setting into the internal sandbox
runtime options.
- Gated run branch setup/push, metadata branch writer creation/push, and
PR branch output on the new settings.
- Enforced invalid combinations: pull requests require an enabled pushed
run branch, and disabling the run branch also disables metadata branch
behavior.
- Updated OpenAPI, the generated TypeScript API client, frontend fixture
data, and docs for the new configuration shape.

## Testing

- `cargo nextest run -p fabro-config -p fabro-types -p fabro-workflow -p
fabro-server`
- `cargo build -p fabro-api`
- `cd lib/packages/fabro-api-client && bun run generate`
- `cd lib/packages/fabro-api-client && bun run typecheck`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test`
- `cargo build --workspace`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D
warnings`
- `cargo insta pending-snapshots`
- `git diff --check`

## Post-Deploy Monitoring & Validation

- Validation window: first 24 hours after release; owner: release
owner/on-call engineer.
- Log queries/search terms: `run_branch`, `meta_branch`,
`clone.enabled`, `skip_clone`, `pull request requires an enabled pushed
run branch`, `metadata branch`.
- Healthy signals: runs without custom branch config continue creating
and pushing run/meta branches; runs with `[run.clone] enabled = false`
start provider sandboxes without cloning; runs with branch pushes
disabled complete without Git push errors.
- Failure signals: increased run startup failures for Docker/Daytona,
unexpected PR creation conflicts, missing metadata for default-config
runs, or validation errors for configurations that previously used
default settings.
- Mitigation trigger: if default-config runs stop producing expected
branch/metadata artifacts or sandbox startup failures increase, roll
back the release or temporarily restore previous defaults while
investigating the run-level setting resolution path.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)

Co-authored-by: Haroldo Olivieri <6575718+haroldolivieri@users.noreply.github.com>
2026-05-12 12:00:30 -04:00
Bryan Helmkamp
2d36ef63cc
ci(web): isolate Bun test files
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
2026-05-11 00:20:42 -04:00
Bryan Helmkamp
4fa4716015
refactor(core): simplify reviewed run cleanup
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.
2026-05-10 22:53:20 -04:00
Bryan Helmkamp
cccb557281
feat(api): unify public run shape
Return canonical Run payloads across run list, board, create, and lifecycle endpoints. Move archive state out of RunStatus and into lifecycle metadata, split sandbox runtime from planned sandbox data, and separate static pull request records from live pull request details.

Regenerate the TypeScript API client and migrate web, CLI, server, store, workflow, and API tests to the new contract.
2026-05-10 20:48:55 -04:00
Bryan Helmkamp
92ce72da6e
refactor(web): simplify profile sessions layout
Split the unified list into separate Browser and CLI panels and drop
provider, login, kind/current badges, and user agent. Each panel shows
just the timestamps that matter, with revoke gated to CLI sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:32:19 -04:00
Bryan Helmkamp
d2623662e0
feat(web): build unified auth sessions page
Render /profile/sessions from the new GET /api/v1/auth/sessions API.
The page shows the current browser session and active CLI sessions in one
list, with revoke buttons gated by the backend-supplied revocable field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:30:00 -04:00
Bryan Helmkamp
acec32cea9
feat(web): build live events page in settings
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>
2026-05-10 14:05:10 -04:00
Bryan Helmkamp
5209d05623
refactor(sandbox): unify run sandbox identity
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.
2026-05-10 13:16:57 -04:00
Bryan Helmkamp
3acfa9eb93
feat(web): add profile sub-navigation with Sessions
Wraps /profile in a left-sidebar layout. Existing profile page becomes
the index; adds a placeholder /profile/sessions. Renames the identity
rows from "IdP issuer"/"IdP subject" to "Issuer"/"Subject".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:40:50 -04:00
Bryan Helmkamp
47f581cc1d
fix(server): improve sandbox service discovery
Fall back to procfs when ss is unavailable, report the discovery source in API metadata, and surface the sandbox install tip in the services UI. Previewable services are ordered first for clearer service selection.
2026-05-10 12:40:38 -04:00
Bryan Helmkamp
a74ae2e7f8
feat(web): add settings sub-navigation with Live Events
Wraps /settings in a left-sidebar layout. Existing settings page becomes
the index; adds a placeholder /settings/live-events. The layout owns the
section h1 and hides the app-shell header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 12:20:42 -04:00
Bryan Helmkamp
7b832c74a8
feat(web): add Services tab to sandbox page
Lists backend-discovered TCP services for a run's sandbox between the
Terminal and Filesystem tabs. Previewable ports open a signed Daytona
URL in a new browser tab; the rest render as Unavailable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:56:05 -04:00
Bryan Helmkamp
1f6965386c
refactor(web): rename Workflows tab to Automations
Repurpose the /workflows tab to manage Automations (e.g., scheduled
workflows). The underlying Workflow domain entity, API types, and
"workflow runs" are unchanged — this is a web-surface rename only:
URLs (/workflows -> /automations), route file names, default-export
component names, page titles, breadcrumbs, and the visible UI labels
on these pages (Create Automation, Search automations..., Run automation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:47:46 -04:00
Bryan Helmkamp
d0f3676d88
fix(web): hide filesystem directory sentinels 2026-05-10 10:41:02 -04:00
Bryan Helmkamp
f607068a99
fix(web): wrap full-screen terminal route in ToastProvider
TerminalView calls useToast() unconditionally, so the chromeless route
crashed with "useToast must be used within a ToastProvider" because the
route lives outside the AppShell that normally provides it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:32:11 -04:00
Bryan Helmkamp
e4093a2e61
feat(web): virtualize sandbox file preview and handle empty files
Wraps the Pierre File renderer in WorkerPoolContextProvider + Virtualizer
so long file previews scroll efficiently and reuse the shared highlighter
worker pool. Adds a content-aware sandbox cacheKey so previews don't
re-highlight unchanged content. Empty files now render an "Empty file"
state instead of mounting an empty File component.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:25:08 -04:00
Bryan Helmkamp
ee3ef3a3b2
feat(web): add "Open in new tab" button to embedded terminal toolbar
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>
2026-05-10 10:23:42 -04:00
Bryan Helmkamp
cb316936e3
feat(web): add chromeless TerminalView and full-screen terminal route
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>
2026-05-10 10:23:11 -04:00
Bryan Helmkamp
5ac9a188c6
refactor(web): drop Start nav tab and reuse sparkles for Workflows
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:22:18 -04:00
Bryan Helmkamp
04b37ed1d3
refactor(web): collapse sandbox mode toggle into panel header rows
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>
2026-05-10 10:01:19 -04:00
Bryan Helmkamp
8bde7d77f5
feat(web): add Daytona VNC mode to Sandbox tab
Adds a third right-column mode (Terminal | Filesystem | VNC) gated to
Daytona sandboxes. The panel POSTs /api/v1/runs/:id/sandbox/vnc, embeds
the signed noVNC preview URL in an iframe with clipboard + fullscreen
allowed, and renders distinct states for unsupported provider (Docker
hides the tab entirely), 409 startup failure (recoverable, "Try again"),
and 404/501 (non-recoverable). A reconnect button refetches the signed
URL when it expires.

Also fixes a Filesystem regression: the previous "skip first effect"
ref guard meant @pierre/trees never received its first resetPaths call
when the listing transitioned from empty to populated, leaving the tree
stuck on the initial empty model. Now the model is always synced via
resetPaths whenever the listing changes; verified live against a
Daytona /workspace + / listing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:21:35 -04:00
Bryan Helmkamp
cc5c505487
feat(web): add read-only Filesystem mode to Sandbox tab
Adds a Filesystem right-column mode alongside Terminal in /runs/:id/sandbox,
selectable via ?mode=filesystem. The persistent SandboxDetails left column
stays visible in both modes. Browses the run sandbox via the existing
list/get file endpoints, previews text files with @pierre/diffs, and falls
back to download-only for binary, oversized, and unreadable files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:48:56 -04:00
Bryan Helmkamp
91a3f552e4
fix(web): widen gap between sandbox state label and native state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:11:58 -04:00