## Summary
- Render the Test column as an icon-only status
(queued/testing/ok/failed) so a long error message no longer expands the
column width.
- Move the failure message into a hover/focus tooltip — wider,
monospaced, and preserving newlines for readable multi-line errors.
## Test plan
- [ ] Visit `/settings/models`, run "Test models", and confirm the Test
column stays narrow regardless of error length.
- [ ] Hover/focus a failed row's icon and verify the tooltip shows the
full multi-line error in monospace.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
`fabro model test` (CLI) probes every configured model with a cheap "Say
OK" prompt and prints a results table. Until now, the equivalent on
`/settings/models` was "open a terminal." This PR adds a single **Test
models** button in the section header that runs the same sweep against
the visible rows and renders per-row results inline. Wire format is the
existing `POST /api/v1/models/{id}/test` — no backend changes.
## Behavior
- One button beside the provider filter + search. Tests *whatever the
table currently shows* (filter + search applied at click time).
- Concurrency cap of 4 to mirror the CLI's `--jobs 4` default.
- Rows render `Queued` → `Testing…` → `Ok` (mint check) or red X +
truncated error (full message on hover via `title`).
- After each sweep, a small `N ok · M failed` chip appears next to the
button (mint when clean, coral on failures).
- Re-clicking starts a fresh sweep over the current view.
## Out of scope (deliberately)
- **No deep-test toggle** — page calls basic mode only; `fabro model
test --deep` still covers that case from the CLI.
- **No per-row Test button** — the page-level sweep replaces it.
- No cancellation, no result persistence across navigation/refresh, no
toast — the inline state *is* the feedback.
## Files
- `apps/fabro-web/app/routes/settings-models.tsx` — `RowState`/`Sweep`
types, `runSweep` worker pool, header button + summary chip, new "Test"
column, `TestStatusCell` component.
- `apps/fabro-web/app/components/state.tsx` — `Spinner` is now exported
(was previously private).
## Test plan
- Click "Test models" with several configured providers → rows flip in
waves of 4; summary lands as `N ok · 0 failed`.
- Revoke a provider's API key, click again → that provider's rows end in
red X with the upstream error in the cell (full text on hover).
- Apply a provider filter, click → only filtered rows test.
- DevTools Network panel → at most 4 in-flight `/models/<id>/test`
requests at any time.
---
[](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>
## Summary
Improves the web UI's React Doctor audit score by separating reusable
helpers from React component modules, tightening effect/state ownership,
and extracting real component boundaries in the install wizard, stage
activity view, run-files diff browser, RunDetail route, and Runs
workspace. The branch removes the previously deferred RunDetail and Runs
giant-component diagnostics without changing RunDetail UX, route
contracts, action ordering, or Runs workspace behavior.
| Metric | Main baseline | Initial PR | Current PR |
|--------|---------------|------------|------------|
| React Doctor score | 63 | 71 | 99 |
| React Doctor errors | 123 | 0 | 0 |
| React Doctor warnings | 241 | 163 | 3 |
| React Doctor diagnostics | 364 | 163 | 3 |
## Changes
- Moves exported helper logic out of component files so Fast
Refresh/component-export rules no longer dominate the audit.
- Adds a targeted React Doctor config exception for React Router route
modules, where non-component exports like route metadata are
intentional.
- Refactors low-risk state/effect patterns: keyed interview question
state, reducer-backed editable run title state, event-owned preview
opening, route-keyed insights editor initialization, refresh timer
ownership, and selection/derived list cleanup.
- Reworks `InstallApp` around an install reducer, a controller hook for
install lifecycle state, and focused wizard step components for LLM,
server, object-store, sandbox, and GitHub setup.
- Moves `RunStages` selected-stage activity into a keyed boundary for
panel/debug detail state while preserving stage activity filters across
navigation.
- Extracts the `RunFiles` loaded diff-browser view from route/query
coordination so the route owns data/URL state and the loaded view owns
rendering.
- Splits `RunDetail` into route-local header, actions, tab shell, docked
controls, model, and lifecycle-toast modules; the actions menu now uses
grouped descriptors instead of a large boolean/callback prop matrix.
- Extracts Runs workspace preference ownership into
`useRunsWorkspacePreferences` and moves toolbar rendering into
`RunsToolbar`, leaving the route focused on data, DnD state, filtering,
and view selection.
- Guards `InsightsEditor` query execution with a latest-run id and
timeout cleanup so stale or unmounted mock query runs cannot overwrite
newer results.
- Adds regression coverage for archived-run deletion from RunDetail and
stale-result handling in InsightsEditor.
- Improves semantic/accessibility coverage with labeled controls, native
meter/section semantics, decorative status dots, and clearer unavailable
copy.
- Removes dead UI code and applies local suppressions only where the
rule is a documented false positive or an intentional imperative
integration boundary.
## Remaining React Doctor warnings
Current score is 99 with 0 errors and 3 warnings. The remaining warnings
are intentionally left for separate judgment rather than mechanical
churn:
- `prefer-useReducer` (3): `AutomationsNew`, `InsightsEditor`, and
`CreateSecretForm` need reducers only if they encode real coupled
transitions, not simple field setters.
## Verification
- `cd apps/fabro-web && bun test app/routes/run-detail.test.ts` -> `22
pass`, `0 fail`
- `cd apps/fabro-web && bun test app/routes/insights-editor.test.tsx
app/routes/runs.preferences.test.tsx` -> `7 pass`, `0 fail`
- `cd apps/fabro-web && bun run typecheck`
- `cd apps/fabro-web && bun test --isolate` -> `490 pass`, `0 fail`
- `cd apps/fabro-web && bunx react-doctor@latest --full --json >
/tmp/fabro-react-doctor-runs-insights.json` -> score `99`, `0` errors,
`3` warnings
- Earlier branch verification also included `cd apps/fabro-web && bun
run build`
- `git diff --check`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context not reported, default reasoning) via
[Codex](https://openai.com/codex)
## Summary
Fixes the Settings Resources concurrency meter so it reports scheduler
capacity usage instead of all non-terminal runs. `/api/v1/system/info`
now exposes `runs.scheduler_slots_used`, computed from the same status
predicate the scheduler uses, while `runs.active` remains unchanged for
existing lifecycle semantics.
The settings page uses only the new slot count, so pending approval runs
and runnable queued runs no longer make the concurrency meter look full.
## Verification
- `cargo build -p fabro-api`
- `cargo nextest run -p fabro-server --features test-support
worker_started_child_run_requires_approval_before_becoming_runnable`
- `cargo nextest run -p fabro-server --features test-support
scheduler_capacity_counts_only_runs_occupying_slots`
- `cargo nextest run -p fabro-server --features test-support
get_system_info_returns_runtime_fields`
- `cargo nextest run -p fabro-server --features test-support
test_app_state_with_options_respects_max_concurrent_runs`
- `cargo nextest run -p fabro-server --features test-support
openapi_conformance`
- `bun test app/routes/settings-monitoring.test.tsx`
- `bun run typecheck`
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context unknown, reasoning unknown) via
[Codex](https://openai.com/codex)
Show the pager only when there's actually more than one page or the
user is past page 1, replacing the hardcoded total >= 25 threshold.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Separates Fabro server secrets into two explicit scopes: **bootstrap**
secrets that come from process env or `server.env`, and **optional
integration** secrets that come exclusively from the vault. This makes
secret resolution simple and predictable, and removes all `process env →
server.env` fallback paths for optional integrations such as GitHub App,
Slack, Daytona, Brave Search, and LLM provider keys.
## What changed
**New `ToolSecrets` struct in `fabro-agent`** — Brave Search API key is
now passed explicitly through `SessionOptions.tool_secrets` rather than
read from process env inside the tool. The standalone CLI reads the key
at the CLI boundary (with an explicit
`#[expect(clippy::disallowed_methods)]` annotation); the server will
read it from the vault. The error message changes from
`"BRAVE_SEARCH_API_KEY environment variable is not set"` to
`"BRAVE_SEARCH_API_KEY is not configured"`.
**`VaultCredentialSource::vault_only` constructor in `fabro-auth`** —
Adds a constructor that passes `|_| None` as the env lookup, ensuring
the server LLM credential source never resolves provider keys from
process env.
**GitHub App secrets move to vault in install flows** — Both the CLI
`fabro install github` path and the browser install finish handler now
write `GITHUB_APP_PRIVATE_KEY`, `GITHUB_APP_CLIENT_SECRET`, and
`GITHUB_APP_WEBHOOK_SECRET` to the vault instead of `server.env`.
Switching strategies removes stale secrets from the other strategy's
storage location. The `vault_set` field type changes from `Vec<(String,
String)>` to `Vec<VaultSecretWrite>` to carry per-secret type metadata
(file vs. token).
**`fabro-vault` gains a `fabro-static` dependency** — Needed so the
vault crate can reference canonical env-var names from the shared
registry without a cycle.
**`GH_TOKEN` fallback removed** — `GITHUB_TOKEN` is now read from the
vault only; the changelog and `server-configuration.mdx` note drops
mention of `GH_TOKEN` as an accepted fallback.
**Version bump** — Workspace crates promoted from `0.244.0-nightly.0` to
`0.244.0`.
**Docs** — Internal strategy doc, public admin docs (Docker, Railway,
server-configuration, security, troubleshooting), and integration docs
(GitHub, Slack, Daytona, Brave Search, LiteLLM, tools reference, models)
all updated to reflect vault-only optional secrets and direct users to
`fabro secret set` rather than process env or `server.env`.
### Plan Summary
- **Task 1** (secret registry) — not yet present in this diff;
classification lives in the places that consume it.
- **Task 3–6** (vault-only lookups for GitHub, Slack, Daytona, LLM) —
implemented via `vault_only` constructor, `tool_secrets` threading, and
install-path changes.
- **Task 7** (Brave Search explicit injection) — `ToolSecrets`,
`register_core_tools` wiring, CLI boundary read.
- **Task 8** (install persistence) — GitHub App secrets written to
vault; token strategy writes `GITHUB_TOKEN` to vault and clears app
vault keys; app strategy clears `GITHUB_TOKEN` vault key.
- **Task 9** (docs) — all public and internal docs updated.
### Fabro Details
<details>
<summary>Ran 0 stages in 155m 26s for $60.85</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| **Total** | **155m 26s** | **$60.85** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
## Summary
Adds three small informational labels to `/settings/models`, all driven
from existing fields on `Provider` and `Model` (no API changes):
- **Priority** — on the configured provider with the highest catalog
`priority`
- **Default** — next to each provider's default model (`model.default`)
- **Small** — next to models flagged as the provider's small default
(`model.small_default`)
A single shared `Label` helper renders them in a subtle uppercase pill
style consistent with other section accents on the page.
## Test plan
- [ ] Visit `/settings/models` and confirm one configured provider shows
a "Priority" label next to its name
- [ ] Confirm each provider has at most one model labeled "Default" in
the Models table
- [ ] Confirm models with `small_default = true` show a "Small" label
(alongside "Default" if both)
- [ ] Confirm unconfigured providers are unaffected (filtered out before
the Models table)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Demo mode remains available via the X-Fabro-Demo header or the
fabro-demo=1 cookie set manually in browser devtools, but the UI
button and the POST /api/v1/demo/toggle endpoint are gone. The
fixture machinery and the auth/me demoMode flag (used by the SPA to
render Automations and the /start landing) are unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors `fabro model list` output below the existing Providers panel.
Server-side provider + query filters, debounced search, sortable
columns, and a hover/focus popover that surfaces model aliases.
Genericizes SortHeader so non-runs tables can reuse it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the Models default with an overview landing at /settings that
shows each settings page as a card with icon, name, and one-line
description, grouped by General / Administration with a divider before
Live Events. Settings nav metadata is restructured into navSections and
exported so the sidebar and landing share a single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Group sidebar items under General and Administration section labels;
default Settings landing page to Models; rename General page to Server
(now at /settings/server); rename Resources to Monitoring (now at
/settings/monitoring) with ChartBarSquare icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PrCard stacked stats, actions+elapsed, and diff stats as three sibling
rows, so +adds/-dels rendered below elapsed. Consolidate into a single
PrCardFooter component so future inline metadata extends one row instead
of stacking another.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the run t-shirt size (XS/S/M/L/XL) in both the main runs
list and the Children sub-tab, visible by default. L renders in
amber and XL in coral to flag risky and unhealthy runs at a glance.
Extracts a shared SizeChip component used by the run header and the
table cell, derives Ord on RunSize so the new sort key (server-side
ListRuns sort) orders by bucket, and reorders TOGGLEABLE_COLUMNS so
the column picker mirrors the visible table order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Now that Size is a first-class column in the runs list, Elapsed is
redundant with it for at-a-glance scanning. Hide Elapsed by default
alongside Updated and Changes; users can still reveal it via the
column picker.
Existing users with stored prefs from the previous "updated,changes"
default keep their stored value, so they'll see both Elapsed and Size
until they toggle Elapsed off (or clear localStorage). New users get
the cleaner default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The grid-based RunRow was the only Children-tab consumer of the
runs-list module's row primitive. When Children adopted the full
RunsListView (table layout) in 4dfcbc0e0, RunRow became unused — the
re-export in runs.tsx was preserved for a release as a precaution, but
nothing imports it. Same for RUNS_LIST_GRID_TEMPLATE, which only the
grid RunRow needed.
Note: automation-runs.tsx still defines its own local RunRow with the
same name; that one is unaffected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated and Changes are now hidden by default in both the main runs list
and the Children sub-tab — they're still toggleable via the column
picker. The column order shifts so Elapsed lives between Updated and
Changes (i.e. after Created/Updated), keeping the time-related columns
grouped on the right.
Defaults are applied in two places: fresh sessions (no stored prefs)
and existing v1 stored prefs that have no `hide` field. Users who
explicitly cleared all hides keep that choice; stored `hide: ""`
serializes round-trip as `?hide=` (empty value) so the URL distinguishes
"show every column" from "use defaults".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The list-view Changes column was always empty because mapRunListItem
never copied additions/deletions from the API's diff payload. Populate
them so rows actually render +/- counts.
The run overview's Changes cell was rendering raw numbers; switch it to
toLocaleString() so it matches the list view's formatting.
Also tighten tabCountBadges in the run-detail test to scope to the
tab-strip's rounded-full badges. The previous selector matched any
tabular-nums span, so the unconditional size chip caused a false
positive in "hides the Files Changed tab badge when diff stats are
absent" after the chip went unconditional in 7d4aa474f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The size chip was gated on billing.total_usd_micros, so it only
appeared after a run reached a terminal state. summary.size is
always present, so render the chip unconditionally and only append
the billed amount to the tooltip when available.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each row in the main runs list and the Children sub-tab now has a
vertical-kebab actions menu, giving a one-click path to manage a single
run without opening it. The menu mirrors the run detail page's Actions
menu but only surfaces the actions that apply to the row's current
state — Approve/Deny for runs awaiting approval, Retry for failed/dead,
Archive for terminal, Unarchive/Delete for archived, Cancel for
in-flight. Copy run ID is always available.
Delete uses the same confirmation dialog as the bulk and detail flows.
Other actions fire directly and broadcast via mutateRunListCaches so
the list refreshes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bulk action toolbar grows: the top-level buttons stay focused on
the common archive/unarchive flow, and an overflow "More" menu now
houses Approve (new) and Delete (moved from the top level).
Approve fans out client-side via Promise.allSettled since there's no
batch approve endpoint yet; the result is reported through the same
summarizeBatchLifecycleAction toast as the other batch actions. Only
runs whose lifecycle.approval.state === "pending" are eligible.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Children tab's bulk action toolbar sits at the bottom of the page,
where the SteerBar was overlapping it. Add a `hideSteerBar` flag to the
Children route handle and skip rendering the fixed bottom bar in
run-detail when set. Keep the InterviewDock visible when there are
pending questions so urgent prompts aren't swallowed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /runs/:id/children tab now gets server-side pagination, sortable
columns, column picker, search/time/archived filters, and bulk
archive/unarchive/delete — same affordances as the main runs list view.
Preferences persist to localStorage under a dedicated key so they don't
collide with the /runs page.
Repo and Workflow filter buttons are intentionally omitted (children
typically share these with the parent), but those columns remain visible
for the cases where workflows fan out across repos.
- New childRunsListPreferences in components/runs-list/preferences.ts
- run-children.tsx fetches via useRunsPage({parentId, ...}) with all
list controls wired up
- Empty state retains the existing "Learn about parent links" CTA
- useChildRuns + queryKeys.runs.children removed (replaced by the
generalized useRunsPage)
- useRetryRun broadcasts via mutateRunListCaches now that the dedicated
children cache key is gone
- Revert board-cache children matcher added in the previous commit
(no longer needed)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the inline runs list view (pagination, sorting, column picker,
selection, bulk actions) from runs.tsx into a self-contained module at
components/runs-list/ so it can be reused by the Children sub-tab and
future run-list surfaces. No behavior change to /runs.
- RunsListView now takes an emptyState slot (Runs page passes RunsLandingEmpty)
- useRunsPage accepts an optional parentId for non-page run lists
- runListCacheMatchers also matches ["runs","children",...] so bulk
archive/delete invalidate children caches automatically
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pending approval is the only action a user can take to unblock a run,
so the Approve action shouldn't be buried in the Actions dropdown.
- Run detail header: render a primary teal "Approve" button beside the
Actions menu when approval is pending; remove the duplicate menu item.
- Board view (/runs): surface `pendingApproval` on RunItem and render
an inline Approve button on cards in the Pending column.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cancelled runs are now eligible for retry alongside other failed and
dead runs. A user who cancels a run and then changes their mind no
longer has to manually re-create it from scratch.
- ensure_retryable drops the FailureReason::Cancelled rejection arm
- canRetry simplifies to failed || dead (still gated by !archived)
- OpenAPI Retry Run description no longer lists cancelled as ineligible
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Loosen vertical spacing between tool rows (space-y-1 → space-y-1.5).
- Add `title={tool.description}` so hovering a tool surfaces the
model-facing description without re-adding inline description text.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the "Full Access" / permission badge section; Tools now conveys
the same surface area more directly.
- Reorder so Tools sits at the bottom (after MCPs).
- Strip each tool row to just a used/not-used indicator and the tool
name — no descriptions, source labels, or category badges.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /settings/sandboxes route surfaces the local, docker, and daytona
runtime sandbox providers using existing useServerSettings(). Mirrors
the /settings/models pattern: enabled providers shown first, disabled
hidden behind a progressive-disclosure toggle. Disabled Daytona row
links to add the DAYTONA_API_KEY secret.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gives each of the four empty-state suggestions a Heroicon to aid
scannability: warning triangle for "Surface errors", bolt for
"Analyze performance", map for "Review key decisions", and lightbulb
for "Suggest improvements".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
Removes the standalone `agent.context_window.snapshot` event and instead
attaches the context-window projection directly to `agent.message`. This
eliminates the async provider token-count API calls that the old
approach required, and simplifies the event log to a single event type
carrying all post-response agent data.
## What Changed and Why
**Before:** After each LLM turn, the agent emitted a separate
`agent.context_window.snapshot` event — first a local estimate, then
potentially a second one after an async `count_input_tokens` call
resolved (or after response usage arrived). This required fingerprint
deduplication state, a `close_token` to cancel in-flight counts, and
frontend handling for the extra event type.
**After:** The `AgentEvent::AssistantMessage` variant carries an
`Option<StageContextWindowProjection>`. The projection is computed
locally at request-build time and then refined using response token
usage when available (`ResponseUsageScaledBreakdown`), or kept as a
`LocalEstimate` when response usage is absent. No provider API calls are
made.
### Plan Summary
- **Task 1:** Added `context_window:
Option<StageContextWindowProjection>` to `AgentMessageProps` (Rust types
+ OpenAPI), removed `AgentContextWindowSnapshotProps` and
`EventBody::AgentContextWindowSnapshot`.
- **Task 2:** Removed the spawned `count_input_tokens` task,
`close_token`, fingerprint sets, and both snapshot-emit methods from
`Session`. Added `context_window_from_response_usage` to
`context_window.rs`; `BuiltRequest` now holds the local projection
instead of the tool list.
- **Task 3:** Workflow conversion copies `context_window` from
`AgentEvent::AssistantMessage` into `AgentMessageProps`; store reducer
reads it from `AgentMessage` instead of the removed snapshot variant and
stamps `event_seq`.
- **Task 4:** GET endpoint tests updated to seed data via
`agent.message` with embedded context-window; endpoint behavior
unchanged.
- **Task 5:** Frontend constant and tests for
`agent.context_window.snapshot` removed; `agent.message` already
invalidates `stageContextWindow` through existing stage-activity
handling. TypeScript client regenerated with the new `AgentMessageProps`
model.
### Key Design Decisions
- **No provider token-count API calls** during normal execution —
context-window accuracy relies on local estimates scaled by response
usage, which is always available for successful turns.
- **Failed-before-response turns** emit no context-window data
(`context_window: None`), matching the old behavior where a snapshot
would have been emitted but response-usage scaling would never arrive.
- `BuiltRequest` drops the `tools` field (only needed for the
now-removed snapshot emission path); the local projection is computed at
build time and stored directly.
### Fabro Details
<details>
<summary>Ran 8 stages in 60m 3s for $55.78</summary>
| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 1s | – | 0 |
| preflight_lint | 2m 16s | – | 0 |
| implement | 30m 39s | $44.82 | 0 |
| simplify_opus | 10m 48s | $4.03 | 0 |
| simplify_gpt | 5m 1s | $6.93 | 0 |
| verify | 8m 47s | – | 0 |
| **Total** | **60m 3s** | **$55.78** | **0** |
</details>
<details>
<summary>Ran <code>ImplementPlan.fabro</code> (11 nodes and 14
edges)</summary>
```dot
digraph ImplementPlan {
graph [
goal="Implement and simplify",
model_stylesheet="
* { model: claude-opus-4-7; }
"
]
rankdir=LR
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
toolchain [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
preflight_lint [label="Preflight Lint", shape=parallelogram, script="cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1", max_retries=0]
fix_lints [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
implement [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD.", model="gpt-55", reasoning_effort="xhigh"]
simplify_opus [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
simplify_gpt [label="Simplify (GPT-55)", prompt="@prompts/simplify.md", model="gpt-55"]
verify [label="Verify", shape=parallelogram, script="git fetch origin main 2>&1 && git merge --no-edit --no-stat origin/main 2>&1 && cargo +nightly-2026-04-14 fmt --all 2>&1 && cargo dev docs refresh 2>&1 && cargo +nightly-2026-04-14 fmt --check --all 2>&1 && { command -v rg >/dev/null 2>&1 || { echo 'rg is required for verify'; exit 127; }; } && ! rg -n 'AuthMode::Disabled|RunAuthMethod|RunSubjectProvenance|\bActorRef\b|\bActorKind\b|AuthenticatedSubject|AuthenticatedService|AuthorizeRunScoped|AuthorizeRunBlob|AuthorizeStageArtifact|AuthorizeCommandLog|auth_method\s*==\s*\"disabled\"' lib/crates apps lib/packages docs/public/api-reference/fabro-api.yaml 2>&1 && cargo +nightly-2026-04-14 clippy --workspace --all-targets -- -D warnings 2>&1 && cargo nextest run --workspace --status-level slow --profile ci 2>&1 && cargo dev docs check 2>&1 && bun install --frozen-lockfile 2>&1 && (cd apps/fabro-web && bun run typecheck) 2>&1 && (cd apps/fabro-web && bun run test) 2>&1 && (cd lib/packages/fabro-api-client && bun run typecheck) 2>&1 && cargo dev build -- -p fabro-cli --release 2>&1", goal_gate=true, retry_target="fixup"]
fixup [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all format, clippy, Rust test, docs, TypeScript typecheck/test, and build failures.", max_visits=3]
start -> toolchain
toolchain -> preflight_compile [condition="outcome=succeeded"]
toolchain -> exit
preflight_compile -> preflight_lint [condition="outcome=succeeded"]
preflight_compile -> exit
preflight_lint -> implement [condition="outcome=succeeded"]
preflight_lint -> fix_lints
fix_lints -> preflight_lint
implement -> simplify_opus -> simplify_gpt -> verify
verify -> exit [condition="outcome=succeeded"]
verify -> fixup
fixup -> verify
}
```
</details>
⚒️ Generated with [Fabro](https://fabro.sh)
---------
Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
Clicking a column header on /runs?view=list silently did nothing.
handleSortClick called updateParam three times in a row; each call
cloned a fresh URLSearchParams from the same closure-captured
searchParams and invoked setSearchParams independently. React Router's
setSearchParams calls don't merge in a single tick, so only the last
one's params landed in the URL -- the sort change was overwritten by
the trailing page-reset. setPageSize had the same shape and was also
quietly losing the size change.
Replace the per-key URL mutator with a reducer-shaped
updatePreferences((prev) => next) that operates on the typed
RunsWorkspacePreferences model. URL and localStorage are derived from
the same next object via the existing converters, and each handler
makes exactly one call -- so concurrent-update races are
structurally impossible. Use setSearchParams((prev) => ...) so the
updater reads the latest committed URL params instead of a closure.
Add page to RunsWorkspacePreferences so the model describes the full
URL view state; strip it before persisting to localStorage since page
is ephemeral. Rename persistRunsWorkspaceSearchParams to
persistRunsWorkspacePreferences to match what it now consumes. Guard
the hydration useEffect with a useRef so it runs only on mount.
Add a regression test that clicks a SortHeader and asserts the URL
gains sort=status while preserving view=list&archived=1, and that a
second click toggles direction=asc.
- Add /automations/new with Basics/Source/Goal/Triggers panels and a
kebab-case Slug auto-derived from Name until the user edits it
- Wire the "Create Automation" button on /automations to the new page
- Move individual automation URL from /automations/<slug> to
/automation/<slug>; /automations and /automations/new are unchanged
- Refresh /settings/secrets/new to use the Panel + Row layout pattern:
breadcrumb header, one field per row, plain footer, no inside-Panel
stacked form
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lifecycle status pill next to the run title now hides when the run
is in the initializing column, mirroring the board view behavior. This
removes the duplicate "Initializing" / "STARTING" indicators that
appeared together on the row during startup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Connect menu (Preview / SSH) on the run detail header was wired up
only in demo mode and the items were never connected to real actions.
Drop the dead UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Pending, Skipped, and Cancelled status pills used `text-fg-muted`
(#4B5768) on `bg-overlay-strong`, producing ~1.7:1 contrast against the
panel — well below WCAG AA. Switch to `text-fg-3` (#A8B5C5) for ~6:1
while keeping the subdued look that distinguishes these states from
active/result tones.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reuses the StagePopover from the stages sidebar so graph nodes reveal
the same handler, timing, model, and status-specific detail on hover.
The graph is server-rendered SVG injected via innerHTML, so listeners
are attached imperatively alongside the existing click handlers; the
popover is portal-positioned via the shared hoverCardStyle helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hovering a stage row reveals handler, timing, model, and status-specific
detail — failure reason for failed/retrying, notes for skipped/partial,
tokens and files-touched for succeeded. Lazy-fetches per-stage events
on first hover via the existing useRunStageEvents hook; HoverCard gains
an openDelay so a cursor sweep doesn't trigger fetches for every row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two icon buttons on the stage events toolbar, to the right of the
model usage label: copy all loaded events as pretty-printed JSON, or
download them as JSONL. Both read from the existing SWR cache, so no
new API surface — for in-flight stages they're a snapshot of what the
client has fetched so far.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the native browser title tooltip on the model chip with a
structured HoverCard listing provider, model, reasoning, and speed.
Shorten the chip label to `model[effort]` (e.g. `gpt-5.5[xhigh]`).
Search expands on focus with a width transition and collapses on blur
when empty. Ghost icon style when collapsed; full input styling slides
in on expand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On /settings/models, unconfigured providers now offer "Add secret →"
alongside "Get API key →", deep-linking to /settings/secrets/new with
the expected vault secret name prefilled. Driven by a new
`expected_secret_name` field on the Provider API, derived from the
first vault credential in the catalog so the suggestion stays in sync
with the catalog instead of being hardcoded on the frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>