mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-06 08:18:58 +00:00
3 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c5dd5772d0
|
Keep run graph zoom/pan when switching tabs (#561)
Switching from a run's Overview tab to another tab and back reset the graph zoom and position to the default. Now it holds. ## Why The viewport (pan and zoom) lived in `RunOverview` component state. Overview and Stages are sibling routes under `runs/:id`, so switching tabs unmounts Overview and drops that state. ## Fix `apps/fabro-web/app/routes/run-overview.tsx`: cache the viewport per run outside the component so it survives the remount, and reset it when the run id changes, since the route instance is reused when only the id changes. Added two tests: viewport restores on remount for the same run, and does not carry across runs. Does not persist across a full page reload (in-memory only). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Bryan Helmkamp <bryan@brynary.com> |
||
|
|
df4fee4dff
|
feat(web): trackpad pan + ⌘-scroll zoom on the run graph (#555)
## What On **Runs → Overview**, the workflow graph now supports the standard Figma/Excalidraw canvas interactions: - **Two-finger scroll → pan** - **⌘/Ctrl + scroll → zoom**, anchored under the cursor (mac trackpad pinch works too — the browser delivers it as `ctrl+wheel`) The graph already had drag-to-pan, stepped zoom (toolbar +/−), and fit-to-window. This adds the missing wheel/trackpad input on top of that existing transform state. https://github.com/user-attachments/assets/15eac98b-2603-44c9-b438-7ee27034ccd7 ## How - **`app/lib/graph-viewport.ts`** (new) — pure, framework-free zoom math: `zoomAtPoint` keeps the point under the cursor fixed while scaling; `clampZoom` + zoom constants. Zoom becomes a continuous float (was a discrete step index) so ⌘-scroll is smooth instead of jumping between steps. Unit-tested (`graph-viewport.test.ts`), including the cursor-anchor invariant. - **`useElementEvent` in `hooks/effects.ts`** (new) — element-scoped, non-passive listener, a sibling to the existing `useWindowEvent`/`useDocumentEvent`. Non-passive is required so the handler can `preventDefault()` the browser's own ⌘-zoom; a JSX `onWheel` can't. - **`routes/run-overview.tsx`** — coalesces zoom+pan into one `view` state (atomic cursor-anchored updates), adds the wheel handler (plain scroll → pan, ⌘/Ctrl → zoom), and `touch-none overscroll-contain` so a horizontal swipe can't trigger browser back-nav. - **`components/graph-toolbar.tsx`** — presentational continuous interface; +/− buttons reuse `zoomAtPoint` (center-anchored). Deletes the now-dead `graph-toolbar-constants.ts`. ## Testing - `bun run typecheck` clean; `bun test` green (incl. 4 new viewport tests). - Verified live against a real 10-node run graph via Chrome DevTools: two-finger pan tracks the scroll delta; ⌘+wheel zoom is cursor-anchored (confirmed even with the cursor over a node); toolbar +/− step ×1.25 and clamp/disable at 200%; fit-to-window sets a continuous scale; node click/hover unaffected. ## Non-goals - **Playground canvas** (`components/playground/canvas`) shares the same hand-rolled pan/zoom pattern and also lacks wheel support — deliberately out of scope; `graph-viewport.ts` is the seam to adopt it later. - **No persistence** — zoom/pan stays ephemeral per visit, as it was before. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|