Commit graph

33 commits

Author SHA1 Message Date
Bryan Helmkamp
2902b8c773
fix(web): harden build version detection 2026-07-25 15:15:25 -04:00
Bryan Helmkamp
695a981f42
feat(web): tell open tabs when a new build ships
A tab left open across a deploy keeps running the previous build's
JavaScript indefinitely. index.html is fetched only on a full page load,
all later navigation is client-side, and hashed bundles are served
`immutable`, so nothing reveals that the code is stale. This produced a
false-positive bug report where two correctly-deployed fixes appeared to
be missing.

Publishes a build id and offers a reload when the running document falls
behind. The toast never reloads on its own; the only automatic reload is
recovery from a chunk that no longer exists.

Build id derivation
-------------------
The obvious approach — hash the emitted asset filenames, which already
embed content hashes — does not work: Bun's minified identifier naming is
not deterministic. Building an unchanged tree twice produces byte-different
output roughly one run in three (same length, ~100k differing bytes, all of
it mangled names). Output hashes therefore move with no source change,
which would fire the toast on redeploys of identical code and train people
to ignore it.

The id is instead derived from the bundle's source inputs, so it changes if
and only if something we control changed. Verified stable across eight
consecutive builds while the entry hash flipped between both variants.

This non-determinism also means two builds of the same commit embed
different bytes into the server binary, which is worth addressing
separately for reproducible builds.

Detection
---------
SWR with `refreshInterval` + `revalidateOnFocus`, per the repo's React
effects policy. SWR does not poll while the document is hidden, so
background tabs stay quiet without extra gating. Unknown state on either
side — missing meta tag, failed fetch, 503 during a dev rebuild — never
produces a prompt.

Stylesheet hashing
------------------
Tailwind's output was stable-named and therefore served `no-cache`, letting
a tab revalidate into new CSS while running old JS. Tailwind purges unused
classes per build, so classes the old bundle still emits could silently
lose their styles. It is now content-hashed and moves with the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:45:26 -04:00
Bryan Helmkamp
737dd75149
feat(web): theme toasts to match the app
Replace Sonner's default richColors palette with a Fabro-themed
FabroToaster: dark panel surface, accent-colored Heroicons type icons
(coral error, mint success, teal info, amber warning), and a themed
close button so persistent error toasts can be dismissed. Extract the
shared config out of the two duplicated <Toaster> mount points.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 08:44:48 -04:00
Bryan Helmkamp
87516c25ce
feat(automations): wire UI to API and auto-start runs from API trigger
Make the Automations area in the web UI functional end-to-end against the
real Automation API, and fix the backend so runs created by an automation's
API trigger actually start instead of sitting in Submitted forever.

Web:
- Reveal the Automations nav tab outside demo mode; drop the now-empty
  demoOnly mechanism.
- List page: render via listAutomations (was workflows mock data); wire
  ellipsis menu to Edit and Delete, with ConfirmDialog + If-Match revision.
  Move Create Automation into the toolbar, switch the trigger select to a
  shared FilterButton, hide the redundant page-header title via a new
  hideTitle handle flag.
- Play button on each card fires createAutomationRun with spinner + toast
  and navigates to the new run.
- New automation form: drop the dead Goal panel and hardcoded repository
  list, post to createAutomation with real triggers.
- Edit automation: new /automations/:id/edit route reusing a shared
  AutomationFormFields component, PUT via replaceAutomation with If-Match.
- Show page: rebuild like a run detail page — breadcrumb, title, chips
  (enabled status, repo+ref, workflow, schedule), Edit + Run actions
  (Run hits createAutomationRun), and a Runs panel using RunsListView
  with URL-driven search/sort/pagination/column-picker like the Children
  sub-tab. Drop the obsolete Definition/Diagram/Runs child routes.

Backend (fabro-server):
- create_automation_run now calls lifecycle::queue_run_start after the
  run is persisted, so the run transitions Submitted → Runnable and the
  scheduler picks it up. Logs a warn and returns the created response if
  start fails (no worse than the prior always-stuck behavior).
- queue_run_start in lifecycle.rs is promoted to pub(super) so sibling
  handlers can reuse it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 20:24:09 -04:00
fabro-sh-0530[bot]
b196a97ac4
Introduce approved effect hooks and migrate direct useEffect calls (#425)
## Summary

Implements the React Effects Policy by creating the approved hook
surface in `hooks/effects.ts` and migrating a broad set of direct
`useEffect` calls across the codebase to either purpose-named hooks or
non-effect patterns.

### Plan Summary

- Add `hooks/effects.ts` exporting `useMountEffect`, `useInterval`,
`useTimeout`, `useDebouncedValue`, `useWindowEvent`, `useDocumentEvent`,
`useDocumentTitle`, `useMediaQuery`, `useLocationHash`, and
`useResizeObserver`
- Extract large imperative effects into purpose-named hooks:
`useTerminalSession`, `useFloatingTooltipMeasurements`,
`useAnnotatedRunGraphSvg`, `useInstallEffects`, and others
- Move install session fetch from a component effect into a SWR query
(`install-query.ts`)
- Replace `useEffect` + `useState` state-derivation patterns with
render-time computation or ref callbacks
- Replace `AskFabroLayoutProvider`/`useAskFabroLayout` context with a
prop callback

## What changed and why

**`hooks/effects.ts`** — the new approved primitive surface. All
internal `useEffect` calls here are intentional; the hooks expose the
*external system* they manage rather than leaking `useEffect` to
component code. `useMediaQuery` and `useLocationHash` use
`useSyncExternalStore` instead of effect + state.

**`useTerminalSession`** — the largest extraction. The 130-line
xterm/WebSocket/ResizeObserver setup block moves from
`terminal-view.tsx` into its own hook, which now owns the `terminalRef`,
`fitRef`, and `socketRef` that previously cluttered the component.
`TerminalConnectionError` and `ConnectionStatus` types are exported from
the hook.

**`useFloatingTooltipMeasurements`** — extracts the `useLayoutEffect` +
ResizeObserver + window resize listener out of `FloatingTooltip`. The
`FloatingTooltipSize` type moves with it so consumers don't need to
import from the component.

**`useInstallSessionQuery` + `useInstallEffects`** — the install session
fetch moves from a component effect to SWR (`install-query.ts`). The
three remaining install effects (token URL scrubbing, GitHub error URL
scrubbing, health-poll restart) move into
`hooks/use-install-effects.ts`. The root-redirect effect is replaced
with a render-time `<Navigate>` gate. The `SessionState` discriminant
now carries `token` so stale query results can be discarded without an
effect chain.

**`SelectionCheckbox`** — `useEffect` setting `input.indeterminate` is
replaced with a ref callback, which runs synchronously after the node is
attached and avoids a stale-frame flash.

**`event-debug.tsx`** — the manual `window.addEventListener("keydown",
...)` pattern is replaced with `useWindowEvent`, removing the
`react-doctor-disable` suppression comments.

**`run-waterfall.tsx`** — the local `useTickingNow` is deleted;
`RunWaterfall` now calls the shared `useTickingNow` from `lib/time` with
the new `active` parameter signature.

**`toast.test.tsx`** — `useEffect(() => onReady?.(api), ...)` in the
test helper is replaced with a direct call during render, which is valid
because `onReady` has no side effects that React cares about.

**`AskFabroSidebar`** — `setIsResizing` from the layout context is
replaced with an `onResizeActiveChange` prop, removing the
`useAskFabroLayout` call and the hidden context coupling from the
sidebar.


### Fabro Details

<details>
<summary>Ran 3 stages in 114m 5s for $95.71</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| work | 103m 3s | $80.42 | 0 |
| audit | 10m 19s | $15.29 | 0 |
| **Total** | **114m 5s** | **$95.71** | **0** |

</details>

<details>
<summary>Ran <code>Goal.fabro</code> (4 nodes and 5 edges)</summary>

```dot
digraph Goal {
    graph [
        goal="Complete the user-provided goal",
        rankdir=LR,
        max_node_visits=30
    ]

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    work [
        label="Work",
        thread_id="goal",
        fidelity="full",
        max_visits=12,
        model="gpt-55",
        reasoning_effort="xhigh",
        prompt="@prompts/continue.md"
    ]

    audit [
        label="Completion Audit",
        thread_id="goal",
        fidelity="full",
        goal_gate=true,
        retry_target="work",
        output_schema="routing",
        output_retries=2,
        max_visits=12,
        model="gpt-55",
        reasoning_effort="xhigh",
        prompt="@prompts/audit.md"
    ]

    start -> work -> audit

    audit -> exit [label="Done", condition="outcome=succeeded"]
    audit -> work [label="Continue", condition="outcome=failed || preferred_label=Continue"]
    audit -> work [label="No clear verdict"]
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
Co-authored-by: Bryan Helmkamp <bryan@brynary.com>
2026-05-27 10:37:29 -04:00
fabro-sh-0530[bot]
c2da22a27c
Replace DIY overlay primitives with Radix UI + Sonner (#424)
Some checks are pending
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) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary

Replaces ~285 lines of hand-rolled Tooltip, HoverCard, and Toast code in
`fabro-web` with battle-tested primitives — gaining real keyboard
accessibility, Radix collision detection, and Sonner's toast lifecycle —
while keeping all 13+ call sites unchanged.

### Plan Summary

- **Tooltip + HoverCard → Radix wrappers**: `@radix-ui/react-tooltip`
and `@radix-ui/react-hover-card` replace the DIY `useHoverAnchor` hook.
A `TooltipProvider` is mounted in `app-shell.tsx` (200ms delay, 300ms
skip-delay for grouped sidebar hovers). `<Tooltip>` self-wraps in a
local provider when rendered outside the shell (tests, isolated mounts).
- **Toast system → Sonner**: `toast.tsx` shrinks to a ~30-line shim
preserving the `{ push, dismiss, clear }` API. `ToastProvider` becomes a
no-op pass-through in DOM contexts; in non-DOM test environments it
renders an `aria-live` fallback backed by `useSonner` so test assertions
still work. The `action` field is dropped (was test-only).
`toast.test.tsx` is rewritten against observable rendered text.
- **CSS-only tooltips → `<Tooltip>`**: Two inline `group-hover/*` blocks
in `settings-models.tsx` are swapped for the new wrapper, gaining
keyboard focus + Esc dismiss + collision avoidance.
- **SVG-anchored hovers → `FloatingTooltip`**: A new
`app/components/floating-tooltip.tsx` helper portals to `document.body`
and computes collision-avoiding `top`/`bottom` placement from a raw
`DOMRect` (no wrappable trigger). It absorbs `hover-card-style.ts`
(deleted) and is used by `run-overview.tsx` and `event-debug.tsx`.

### What changed and why

**`FloatingTooltip`** handles the two SVG/Graphviz hover sites where
there is no React trigger element to wrap — only a `DOMRect` measured
from DOM events. It uses `useLayoutEffect` + `ResizeObserver` to measure
its own rendered size before applying final position, so it never clips
at viewport edges. This is the one place a `useLayoutEffect` is
intentional and documented.

**`Tooltip` provider fallback**: Radix throws if `<Tooltip>` renders
without an ancestor `TooltipProvider`. Rather than requiring every test
to mount the shell, the component detects provider presence via context
and injects a local one when needed.

**Toast shim backward-compat**: `ToastProvider` previously accepted
`autoDismissMs` as a prop; that prop is silently dropped. The `action`
field on `ToastInput` is removed (only one test referenced it —
`run-detail.test.ts` is updated accordingly). All other consumers
compile without changes.

**CSP fix** (bundled): `img-src` gains
`https://avatars.githubusercontent.com` to allow GitHub avatar images,
with the corresponding integration-test assertion updated.


### Fabro Details

<details>
<summary>Ran 8 stages in 54m 1s for $32.86</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 1s | – | 0 |
| preflight_compile | 2m 6s | – | 0 |
| preflight_lint | 2m 23s | – | 0 |
| implement | 21m 38s | $22.48 | 0 |
| simplify_opus | 13m 51s | $7.00 | 0 |
| simplify_gpt | 3m 54s | $3.38 | 0 |
| verify | 9m 35s | – | 0 |
| **Total** | **54m 1s** | **$32.86** | **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>
2026-05-26 23:03:06 -04:00
Bryan Helmkamp
62f0b3e7d1
refactor(web): improve React Doctor score (#405)
## 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`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (context not reported, default reasoning) via
[Codex](https://openai.com/codex)
2026-05-25 22:41:37 -04:00
Bryan Helmkamp
2a2b410802
feat: remove demo-mode toggle button and endpoint
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>
2026-05-25 15:35:37 -04:00
Bryan Helmkamp
95ca1b9619
Use ClockIcon for Automations nav entry
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 09:32:35 -04:00
Bryan Helmkamp
1cd5d89316
Runs list view: table layout, server-side sort/pagination, redesigned toolbar (#367)
## Summary

Overhauls the `/runs` list view and consolidates the two runs endpoints
that backed it.

**API**
- Removes `GET /api/v1/boards/runs`, `PaginatedBoardRunList`, and
`BoardColumnDefinition`. The board view is now a pure frontend
rendering.
- `GET /api/v1/runs` gains `status` (repeatable `BoardColumn`), `sort`
(`created_at | updated_at | status | elapsed`, default `created_at`),
and `direction` (`asc | desc`, default `desc`).
- `BoardColumn` enum gains `removing`; default behavior hides
Removing-status runs, opt in with `?status=removing`.
- `PaginationMeta` gains an optional `total: int64`; `list_runs` fills
it in (free — it already filters all runs in memory before paging).

**List view UI**
- Renders as a real `<table>` with column headings instead of horizontal
cards.
- Sortable Status, Elapsed, Created, and Updated headers — click to
toggle direction, click another to switch sort key (resets to desc). URL
params drive `sort`/`direction`/`page`/`size`.
- New pager footer with rows-per-page selector (10/25/50/100), `Page X
of Y`, and first/prev/next/last icon buttons.
- Toolbar redesigned into left (search + filter buttons for
Time/Repo/Workflow + archived toggle) and right (column picker + view
toggle) sections. Filter buttons use Headless UI `Menu` popovers; the
column picker uses Headless UI `Listbox` with `multiple` for
multi-select. Hidden columns persist via `?hide=...`.

**Tests**
- 589 server tests pass, including new coverage for status filter
(single + repeated), Removing opt-in, sort × direction with `id desc`
tiebreak, and status-bucket sorting.
- Frontend tests updated for the matcher-based cache invalidation and
the new `buildBoardColumns` signature; 435 pass (3 pre-existing
`RunDetail full-height` failures unrelated to this change).

## Test plan

- [ ] `cargo build --workspace`
- [ ] `cargo nextest run -p fabro-server`
- [ ] `cd lib/packages/fabro-api-client && bun run generate` — no diff
(already regenerated and committed)
- [ ] `cd apps/fabro-web && bun run typecheck && bun test`
- [ ] Manual: visit `/runs` — board view still renders all columns in
canonical order, Removing runs hidden, archived toggle works.
- [ ] Manual: visit `/runs?view=list` — table renders with sortable
headers; clicking a header updates URL; pager advances; changing
rows-per-page resets to page 1; column picker hides/shows columns and
round-trips via `?hide=`.
- [ ] Manual: `curl '/api/v1/boards/runs'` → 404; `curl
'/api/v1/runs?status=removing'` returns only removing runs; `curl
'/api/v1/runs?sort=status&direction=asc'` returns runs grouped by status
bucket.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 14:25:58 -04:00
Bryan Helmkamp
a8900c465d
feat: make the Ask Fabro sidebar resizable
Add a drag handle on the left edge of the docked Ask Fabro panel. The
user can widen the panel up to 2x its default width and no narrower than
the default. The chosen width persists across open/close.

An `isResizing` flag on the layout context lets `<main>` and the steer
bar drop their width transitions during a drag so the layout tracks the
cursor instead of trailing it.

Also bundles in-progress sidebar wiring: a collapsed tool-call summary
renderer and the remark-gfm dependency for GitHub-flavored Markdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:55:41 -04:00
Bryan Helmkamp
4e4ab091d4
feat(web): add Ask Fabro sidebar and settings panels (#346)
## Summary

Adds the prototype Ask Fabro docked sidebar to run detail pages behind
`?ask=1`, with app-shell layout coordination so opening the sidebar
shifts content instead of covering it. The branch also improves settings
visibility with active run concurrency on Resources and a Project
Management integrations placeholder.

## Changes

- Add a shared Ask Fabro layout context so the app shell can inset main
content by the docked sidebar width.
- Gate the run detail Ask Fabro button and sidebar behind `?ask=1`,
keeping the bottom steer/interview bar aligned while the sidebar is
open.
- Poll system info on the Resources page to show active runs against the
scheduler limit.
- Add a Project Management panel with Linear marked as coming soon.

## Verification

Not run; PR opened from the existing branch without changing code.

---

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 19:36:19 -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
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
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
dae5ea4c80
feat(web): add demo-only Start tab to app shell nav
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:55:25 -04:00
Bryan Helmkamp
025f41dddd
feat(web): add Profile page to user menu
Surfaces session info from useAuthMe (name, avatar, username, email,
IdP issuer/subject, profile URL) on a new /profile route, structured as
Basics + Identity panels matching the Settings page layout. Adds a
Profile link to the desktop dropdown and mobile menu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:33:11 -07:00
Bryan Helmkamp
de9656dfb7
fix(web): propagate definite height through full-height layout chain
The full-height shell used min-h-dvh, which only sets a minimum and
leaves height: auto. CSS percentage heights (h-full) don't resolve
against an auto parent, so every descendant that relied on h-full
collapsed to its content size — leaving the run stages column
separator, events list, and detail panel ending mid-page instead of
reaching the window bottom.

Switch the shell to h-dvh, make the run-detail outlet wrapper a flex
column, and replace h-full with flex-1 on the run-stages and
run-files roots so they grow via flex sizing within the column. The
height chain is now: shell h-dvh → main flex-1 → layout div h-full →
run-detail h-full → outlet wrapper flex-1 flex-col → page root flex-1
→ children fill via flex stretch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:43:03 -07:00
Bryan Helmkamp
603a64810c
fix(web): virtualize run file diffs consistently
Some checks are pending
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) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Always route non-empty Files Changed views through Pierre's Virtualizer and worker pool, with full-height layout propagation and stable per-file cache keys. Copy Pierre worker assets during the web build so the static worker URL resolves in production.
2026-05-05 16:41:39 -04:00
Bryan Helmkamp
b5f200d701
chore(web): namespace static images under /images and skip in HTTP logs
Move favicon, logo, logotype, and PNG icons from /public/ root to
/public/images/ so the HTTP log middleware can drop them by path
prefix. Extends the existing /assets/ skip in http_log_middleware to
cover /images/ as well, removing favicon/logo entries from the server
log without filtering by extension (which would risk muting future
extension-suffixed API routes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:57:14 -04:00
Bryan Helmkamp
a1f032e166
refactor(web): move server state to SWR
Replace React Router loader/action state paths with SWR query and mutation hooks.

Add targeted run and board EventSource managers that invalidate SWR keys, and refresh embedded SPA assets.
2026-04-25 07:16:41 -04:00
Bryan Helmkamp
c769b2a30c
Merge remote-tracking branch 'origin/main'
# Conflicts:
#	apps/fabro-web/app/routes/run-files.tsx
#	lib/crates/fabro-spa/assets/assets/app.css
#	lib/crates/fabro-spa/assets/index.html
2026-04-20 09:28:27 -04:00
Bryan Helmkamp
5370a64f96
refactor(web): drop light mode, go dark-only
Removes the light/dark toggle infrastructure in favor of a single dark
theme. Deletes the theme context, boot script, light-mode CSS overrides,
logotype-light asset, and the pierre-light diff theme. Collapses
graph-theme into a single constant. Adds scheme-only-dark on <html> so
native controls and the server-injected Graphviz @media query render
dark.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:27:08 -04:00
Bryan Helmkamp
8259b7445e
feat(web): add run lifecycle actions
Expose cancel, archive, and unarchive from the run detail view,
surface blocked-question context, and route run-detail and run-files
notifications through a single shared toast provider.

This also refreshes the embedded SPA bundle and marks the lifecycle
actions plan complete.
2026-04-20 08:43:30 -04:00
Bryan Helmkamp
647da0dddf
feat(app-shell): promote Settings to a top-level nav item
Previously Settings was reachable only via direct URL or logout menu.
Add it to the nav (visible in both demo and real modes) and drop the
now-redundant in-page title so the shell header supplies the heading
instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 17:03:27 -04:00
Bryan Helmkamp
476b20176e
style(app-shell): tighten header and nav surface treatment
- drop the semi-transparent bg-panel/50 on the top nav so the page
  background stops bleeding through
- simplify the header separator to after:border-b with a single
  bottom inset
- raise the page title from text-lg/6 to text-xl for room to breathe
- add isolate to the root so Headless UI portals don't fight the
  header's stacking context

Leaves the demo-mode beaker toggle visible in real mode as a
follow-up; gating it cleanly needs a new feature flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:44:37 -04:00
Bryan Helmkamp
74578c22bc
feat(web): add empty state to runs page and fix logo link
Show a getting-started empty state with quick start commands and
resource links (docs, Discord) when there are zero runs. Link the
logo to /runs in non-demo mode instead of /start.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 08:02:18 -04:00
Bryan Helmkamp
bb63182dba
fix(web): use plain HTML form for logout to avoid React Router 500
React Router's <Form> intercepts submissions and tries to match the
action URL against client-side routes. Since /auth/logout is a
server-only route, this caused a 500 error on sign out.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 07:01:56 -04:00
Bryan Helmkamp
da88a47595 feat: wire web UI to real server with demo mode toggle
Server changes:
- Add /boards/runs to demo routes (delegates to list_runs)
- Fix demo get_run_status to return StoreRunSummary shape matching OpenAPI spec
- Enrich real /boards/runs to return RunListItem shape with board column mapping
  (Running->working, Paused->pending, Completed->merge; others excluded)
- Update existing tests that asserted old RunStatusResponse fields from /boards/runs

Web UI changes:
- Add DemoModeProvider context and useDemoMode hook
- Hide Workflows/Insights nav items in production mode via getVisibleNavigation
- Change run-detail loader to use /runs/{id} directly instead of searching /boards/runs
- Add mapRunSummaryToRunItem for mapping server response to UI shape
- Add Graph tab, hide Stages tab in production mode, always hide Files tab
- Make run-overview and run-graph loaders resilient to 501 via apiJsonOrNull
- Add isNotImplemented and apiJsonOrNull helpers to api.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 04:46:08 -04:00
Bryan Helmkamp
17e59992d5 chore: remove unused verification, retros, and sessions endpoints
These endpoints had zero CLI callers and served only the web UI demo.
Verification and retros were `not_implemented` stubs in real mode;
sessions had an in-memory implementation but no CLI usage. Removing
them shrinks the API surface and eliminates ~9,000 lines of dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 17:52:51 -04:00
Bryan Helmkamp
b56b82d34b Cut over Fabro web app to a server-backed SPA
Replace the old React Router SSR setup with a static SPA build served by
fabro-server, move setup and GitHub auth handling into Rust, and update the
default local web URL and stale Arc-era references to match the Fabro name.
2026-04-01 21:36:01 -07:00
brynary-fabro[bot]
a7faf32e01 Rename [feature_flags] to [features] (#11)
This PR renames the `[feature_flags]` configuration section to
`[features]` and the corresponding `FeatureFlags` type to `Features`
across the entire stack. The change touches the OpenAPI spec (source of
truth), Rust config/API crates, the generated TypeScript API client, the
web application, and the demo config file to ensure consistency.

On the Rust side, the `FeatureFlags` struct becomes `Features` in
`fabro-config`, and all field references (`config.feature_flags` →
`config.features`) are updated in `fabro-api` demo/test code along with
the relevant unit tests. On the TypeScript side, the generated client
reflects the OpenAPI rename (`feature-flags.ts` → `features.ts`,
`FeatureFlags` → `Features`), and manual edits in `fabro-web` update the
`AppConfig` interface, defaults constant (`FEATURES_DEFAULTS`), loader
data destructuring, and template usage throughout `config.server.ts`,
`app-shell.tsx`, and `start.tsx`.

The regeneration also picked up an unrelated new `GitHubConfiguration`
schema that was already present in the OpenAPI spec but hadn't been
generated yet, resulting in the new `git-hub-configuration.ts` file and
its addition to the server configuration type.

### Fabro Details

<details>
<summary>Ran 7 stages in 15m 4s for $3.08</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 0s | – | 0 |
| preflight_lint | 0s | – | 0 |
| implement | 0s | $2.18 | 0 |
| simplify | 0s | $0.90 | 0 |
| verify | 0s | – | 0 |
| **Total** | **15m 4s** | **$3.08** | **0** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (10 nodes and 13
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    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 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -- -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."]
    simplify          [label="Simplify", prompt="@prompts/simplify.md"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -- -D warnings 2>&1 && cargo test 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify -> verify
    verify -> exit  [condition="outcome=success"]
    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>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:54:40 -04:00
Bryan Helmkamp
3a6ec9b9ef Rename Arc to Fabro in TypeScript/JavaScript
Rename directories (arc-web → fabro-web, arc-api-client → fabro-api-client),
update package names, import paths, TS-only identifiers (theme key, session
cookie, demo cookie, OAuth state, db filename, mock data), and supporting
files (Dockerfile, docker-compose, entrypoint, CI workflow, CLAUDE.md).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:13:18 -04:00