checkpoint

⚒️ Generated with [Fabro](https://fabro.sh)
This commit is contained in:
Fabro 2026-05-26 22:47:27 -04:00
parent 9a8f8a8c46
commit 7b161e9b08
5 changed files with 1079 additions and 31 deletions

569
run.json

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,369 @@
diff --git a/apps/fabro-web/app/components/event-debug.tsx b/apps/fabro-web/app/components/event-debug.tsx
index 75c8a1c7d..2d9ccb8c6 100644
--- a/apps/fabro-web/app/components/event-debug.tsx
+++ b/apps/fabro-web/app/components/event-debug.tsx
@@ -418,11 +418,7 @@ function DnaPopover({
}) {
const category = debugCategory(event.event);
return (
- <FloatingTooltip
- rect={anchorRect}
- placement="top"
- className="whitespace-nowrap rounded-md bg-panel-alt px-2.5 py-1 text-xs text-fg shadow-lg outline-1 -outline-offset-1 outline-line-strong"
- >
+ <FloatingTooltip rect={anchorRect} placement="top">
{`${debugCategoryLabel(category)} · ${friendlyEventName(event.event)} · ${formatElapsed(event.ts, runStart)}`}
</FloatingTooltip>
);
@@ -628,11 +624,7 @@ function ThreadDnaPopover({
const duration =
item.durationMs > 0 ? formatThreadDuration(item.durationMs) : "instant";
return (
- <FloatingTooltip
- rect={anchorRect}
- placement="top"
- className="whitespace-nowrap rounded-md bg-panel-alt px-2.5 py-1 text-xs text-fg shadow-lg outline-1 -outline-offset-1 outline-line-strong"
- >
+ <FloatingTooltip rect={anchorRect} placement="top">
{`${THREAD_CATEGORY_LABEL[item.category]} · ${item.label} · ${elapsed} · ${duration}`}
</FloatingTooltip>
);
diff --git a/apps/fabro-web/app/components/floating-tooltip.tsx b/apps/fabro-web/app/components/floating-tooltip.tsx
index 4b524e3c5..50515fd8c 100644
--- a/apps/fabro-web/app/components/floating-tooltip.tsx
+++ b/apps/fabro-web/app/components/floating-tooltip.tsx
@@ -1,6 +1,5 @@
import {
useLayoutEffect,
- useMemo,
useRef,
useState,
type CSSProperties,
@@ -12,27 +11,22 @@ type FloatingTooltipPlacement = "top" | "bottom";
const VIEWPORT_MARGIN = 12;
const OFFSET = 8;
+const DEFAULT_CLASS_NAME =
+ "whitespace-nowrap rounded-md bg-panel-alt px-2.5 py-1 text-xs text-fg shadow-lg outline-1 -outline-offset-1 outline-line-strong";
function clamp(value: number, min: number, max: number): number {
if (max < min) return (min + max) / 2;
return Math.min(Math.max(value, min), max);
}
-function viewportSize() {
- return {
- height: window.innerHeight,
- width: window.innerWidth,
- };
-}
-
function resolvePlacement(
rect: DOMRect,
placement: FloatingTooltipPlacement,
height: number,
+ viewportHeight: number,
): FloatingTooltipPlacement {
if (height <= 0) return placement;
- const { height: viewportHeight } = viewportSize();
const fitsTop = rect.top - OFFSET - height >= VIEWPORT_MARGIN;
const fitsBottom = rect.bottom + OFFSET + height <= viewportHeight - VIEWPORT_MARGIN;
@@ -47,7 +41,8 @@ function floatingStyle(
placement: FloatingTooltipPlacement,
size: { height: number; width: number },
): CSSProperties {
- const { height: viewportHeight, width: viewportWidth } = viewportSize();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
const centerX = rect.left + rect.width / 2;
const availableWidth = Math.max(0, viewportWidth - VIEWPORT_MARGIN * 2);
const width = size.width > 0 ? Math.min(size.width, availableWidth) : 0;
@@ -57,7 +52,7 @@ function floatingStyle(
const left = width > 0
? clamp(centerX, minCenter, maxCenter)
: clamp(centerX, VIEWPORT_MARGIN, viewportWidth - VIEWPORT_MARGIN);
- const resolvedPlacement = resolvePlacement(rect, placement, size.height);
+ const resolvedPlacement = resolvePlacement(rect, placement, size.height, viewportHeight);
if (resolvedPlacement === "top") {
const top = size.height > 0
@@ -86,7 +81,7 @@ export function FloatingTooltip({
rect,
placement,
children,
- className = "",
+ className = DEFAULT_CLASS_NAME,
}: {
rect: DOMRect;
placement: FloatingTooltipPlacement;
@@ -95,22 +90,18 @@ export function FloatingTooltip({
}) {
const ref = useRef<HTMLDivElement>(null);
const [size, setSize] = useState({ height: 0, width: 0 });
- const portalTarget = typeof document === "undefined" ? null : document.body;
- const style = useMemo(
- () =>
- typeof window === "undefined"
- ? undefined
- : floatingStyle(rect, placement, size),
- [placement, rect, size],
- );
useLayoutEffect(() => {
const node = ref.current;
- if (!node || typeof window === "undefined") return;
+ if (!node) return;
const updateSize = () => {
const next = node.getBoundingClientRect();
- setSize({ height: next.height, width: next.width });
+ setSize((prev) =>
+ prev.height === next.height && prev.width === next.width
+ ? prev
+ : { height: next.height, width: next.width },
+ );
};
updateSize();
@@ -124,19 +115,19 @@ export function FloatingTooltip({
resizeObserver?.disconnect();
window.removeEventListener("resize", updateSize);
};
- }, [children, rect]);
+ }, []);
- if (!portalTarget || !style) return null;
+ if (typeof document === "undefined") return null;
return createPortal(
<div
ref={ref}
role="tooltip"
- style={style}
+ style={floatingStyle(rect, placement, size)}
className={`pointer-events-none fixed z-50 ${className}`}
>
{children}
</div>,
- portalTarget,
+ document.body,
);
}
diff --git a/apps/fabro-web/app/components/toast.test.tsx b/apps/fabro-web/app/components/toast.test.tsx
index bc27f7b20..48f67514d 100644
--- a/apps/fabro-web/app/components/toast.test.tsx
+++ b/apps/fabro-web/app/components/toast.test.tsx
@@ -203,7 +203,7 @@ describe("useToast", () => {
let renderer: TestRenderer.ReactTestRenderer | null = null;
await act(async () => {
renderer = TestRenderer.create(
- <ToastProvider autoDismissMs={1000}>
+ <ToastProvider>
<span>wrapped child</span>
</ToastProvider>,
);
diff --git a/apps/fabro-web/app/components/toast.tsx b/apps/fabro-web/app/components/toast.tsx
index 561edde07..f45c95ff4 100644
--- a/apps/fabro-web/app/components/toast.tsx
+++ b/apps/fabro-web/app/components/toast.tsx
@@ -33,7 +33,6 @@ function push(toast: ToastInput): string {
} else {
sonnerToast(toast.message, options);
}
-
return id;
}
@@ -47,16 +46,17 @@ const toastApi: ToastContextValue = {
},
};
-export function ToastProvider({
- children,
-}: {
- children: ReactNode;
- autoDismissMs?: number;
-}) {
+/**
+ * No-op wrapper retained so existing test harnesses and the standalone terminal
+ * route can keep their <ToastProvider> mount points. In a browser the real
+ * <Toaster /> is mounted globally in AppShell; in non-DOM test environments we
+ * render an aria-live fallback that subscribes to the Sonner store so test
+ * assertions can read the toast text.
+ */
+export function ToastProvider({ children }: { children: ReactNode }) {
if (typeof document !== "undefined") {
return <>{children}</>;
}
-
return (
<>
{children}
@@ -72,7 +72,6 @@ export function useToast(): ToastContextValue {
function NonDomToastOutput() {
const { toasts } = useSonner();
if (toasts.length === 0) return null;
-
return (
<output aria-live="polite">
{toasts.map((toast) => (
diff --git a/apps/fabro-web/app/components/ui.tsx b/apps/fabro-web/app/components/ui.tsx
index 8318224a3..8b3daa5de 100644
--- a/apps/fabro-web/app/components/ui.tsx
+++ b/apps/fabro-web/app/components/ui.tsx
@@ -138,7 +138,10 @@ export function ConfirmDialog({
);
}
-const TooltipProviderMountedContext = createContext(false);
+const TOOLTIP_DELAY_DURATION = 200;
+const TOOLTIP_SKIP_DELAY_DURATION = 300;
+
+const HasTooltipProviderContext = createContext(false);
type TooltipProviderProps = ComponentProps<typeof TooltipPrimitive.Provider>;
@@ -146,15 +149,25 @@ function canUseOverlayDom() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
-export function TooltipProvider({ children, ...props }: TooltipProviderProps) {
+export function TooltipProvider({
+ delayDuration = TOOLTIP_DELAY_DURATION,
+ skipDelayDuration = TOOLTIP_SKIP_DELAY_DURATION,
+ children,
+ ...props
+}: TooltipProviderProps) {
if (!canUseOverlayDom()) {
return <>{children}</>;
}
-
return (
- <TooltipProviderMountedContext.Provider value={true}>
- <TooltipPrimitive.Provider {...props}>{children}</TooltipPrimitive.Provider>
- </TooltipProviderMountedContext.Provider>
+ <HasTooltipProviderContext.Provider value={true}>
+ <TooltipPrimitive.Provider
+ delayDuration={delayDuration}
+ skipDelayDuration={skipDelayDuration}
+ {...props}
+ >
+ {children}
+ </TooltipPrimitive.Provider>
+ </HasTooltipProviderContext.Provider>
);
}
@@ -165,40 +178,34 @@ export function Tooltip({
label: ReactNode;
children: ReactNode;
}) {
- const hasProvider = useContext(TooltipProviderMountedContext);
+ // Tests and isolated mounts may render <Tooltip> without an ancestor
+ // <TooltipProvider>; Radix throws in that case, so supply a local provider.
+ const hasProvider = useContext(HasTooltipProviderContext);
if (!canUseOverlayDom()) {
return <span className="inline-flex">{children}</span>;
}
- const tooltip = (
+ const root = (
<TooltipPrimitive.Root>
- <TooltipPrimitive.Trigger asChild className="inline-flex">
- {children}
+ <TooltipPrimitive.Trigger asChild>
+ <span className="inline-flex">{children}</span>
</TooltipPrimitive.Trigger>
- {typeof document !== "undefined" && (
- <TooltipPrimitive.Portal>
- <TooltipPrimitive.Content
- side="top"
- align="center"
- sideOffset={6}
- collisionPadding={12}
- className="pointer-events-none z-50 max-w-[min(32rem,calc(100vw-1.5rem))] rounded-md bg-panel px-2 py-1 text-xs text-fg-2 shadow-lg outline-1 -outline-offset-1 outline-line-strong"
- >
- {label}
- </TooltipPrimitive.Content>
- </TooltipPrimitive.Portal>
- )}
+ <TooltipPrimitive.Portal>
+ <TooltipPrimitive.Content
+ side="top"
+ align="center"
+ sideOffset={6}
+ collisionPadding={12}
+ className="pointer-events-none z-50 max-w-[min(32rem,calc(100vw-1.5rem))] rounded-md bg-panel px-2 py-1 text-xs text-fg-2 shadow-lg outline-1 -outline-offset-1 outline-line-strong"
+ >
+ {label}
+ </TooltipPrimitive.Content>
+ </TooltipPrimitive.Portal>
</TooltipPrimitive.Root>
);
- if (hasProvider) return tooltip;
-
- return (
- <TooltipPrimitive.Provider delayDuration={200} skipDelayDuration={300}>
- {tooltip}
- </TooltipPrimitive.Provider>
- );
+ return hasProvider ? root : <TooltipProvider>{root}</TooltipProvider>;
}
/**
@@ -220,25 +227,22 @@ export function HoverCard({
if (!canUseOverlayDom()) {
return <span className={className}>{children}</span>;
}
-
return (
<HoverCardPrimitive.Root openDelay={openDelay} closeDelay={0}>
- <HoverCardPrimitive.Trigger asChild className={className}>
- {children}
+ <HoverCardPrimitive.Trigger asChild>
+ <span className={className}>{children}</span>
</HoverCardPrimitive.Trigger>
- {typeof document !== "undefined" && (
- <HoverCardPrimitive.Portal>
- <HoverCardPrimitive.Content
- side="bottom"
- align="start"
- sideOffset={6}
- collisionPadding={12}
- className="pointer-events-none z-50 max-w-[18rem] rounded-lg bg-panel p-3 text-xs text-fg-2 shadow-xl outline-1 -outline-offset-1 outline-line-strong"
- >
- {content}
- </HoverCardPrimitive.Content>
- </HoverCardPrimitive.Portal>
- )}
+ <HoverCardPrimitive.Portal>
+ <HoverCardPrimitive.Content
+ side="bottom"
+ align="start"
+ sideOffset={6}
+ collisionPadding={12}
+ className="pointer-events-none z-50 max-w-[18rem] rounded-lg bg-panel p-3 text-xs text-fg-2 shadow-xl outline-1 -outline-offset-1 outline-line-strong"
+ >
+ {content}
+ </HoverCardPrimitive.Content>
+ </HoverCardPrimitive.Portal>
</HoverCardPrimitive.Root>
);
}
diff --git a/apps/fabro-web/app/layouts/app-shell.tsx b/apps/fabro-web/app/layouts/app-shell.tsx
index edc5eac19..ca0cbe417 100644
--- a/apps/fabro-web/app/layouts/app-shell.tsx
+++ b/apps/fabro-web/app/layouts/app-shell.tsx
@@ -64,7 +64,7 @@ export default function AppShell() {
return (
<DemoModeProvider value={demoMode}>
- <TooltipProvider delayDuration={200} skipDelayDuration={300}>
+ <TooltipProvider>
<AskFabroLayoutProvider>
<div
className={classNames(

View file

@ -0,0 +1,6 @@
{
"outcome": "succeeded",
"notes": "Stage completed: simplify_opus",
"failure_reason": null,
"timestamp": "2026-05-27T02:43:28.933767Z"
}

View file

@ -0,0 +1,161 @@
Goal: # Replace DIY overlay primitives in fabro-web
## Context
`apps/fabro-web` hand-rolls Tooltip, HoverCard, and a Toast system. ~285 lines of overlay code with weak collision detection, no keyboard a11y on the CSS-only tooltips, and a custom Toast context that no longer earns its complexity. Already on `@headlessui/react` for Dialog/Menu — Headless doesn't ship Tooltip/HoverCard/Toast, so this is a real gap, not redundancy.
Goal: delete the DIY code, gain real a11y/positioning, keep call sites stable.
## Scope (3 areas)
### 1. Tooltip + HoverCard → Radix wrappers
Add `@radix-ui/react-tooltip` and `@radix-ui/react-hover-card`.
Keep the public API (`<Tooltip label={x}>{children}</Tooltip>`, `<HoverCard content={x}>{children}</HoverCard>`) by reimplementing the two components in `app/components/ui.tsx` as thin Radix wrappers. All 13 existing call sites remain unchanged.
- Delete `useHoverAnchor` (ui.tsx:141-179).
- Mount one `TooltipProvider` in `app/layouts/app-shell.tsx` (delay 200, skipDelayDuration 300) so siblings share a delay group.
- HoverCard wrapper passes `openDelay` (default 0, stage-sidebar still passes 200) → Radix `openDelay`.
- Keep `PopoverHeader` / `PopoverRows` / `PopoverRow` unchanged — presentational, used inside HoverCard `content`.
Call sites (do not touch): `run-billing`, `settings-live-events`, `run-sandbox/{services,vnc,filesystem}-panel`, `terminal-view`, `size-chip`, `run-summary-panel`, `event-debug` (Tooltip wrapper use), `meta-bar`, `human-qa`, `run-table-row`, `run-waterfall`, `stage-sidebar`, `run-stages`, `run-detail/header`.
### 2. Toast system → Sonner
Add `sonner`. Mount `<Toaster richColors position="bottom-right" />` in `app/layouts/app-shell.tsx` next to the new `TooltipProvider`.
Replace `app/components/toast.tsx` with a tiny shim that preserves the current API:
```ts
// useToast() returns { push, dismiss, clear }
// push({ message, tone, autoDismissMs }) → toast(msg) / toast.error(msg) / toast(msg, { duration })
```
Keep the shim so the 10 consumers + `useRunToasts` need zero changes. `action` field unused in production — drop from the type (only the test referenced it).
Rewrite `toast.test.tsx` against the shim's observable behavior (rendered text, error persistence) rather than `data-toast-id`. Other tests that wrap in `<ToastProvider>` keep working because the shim re-exports a no-op `ToastProvider` (sonner's `Toaster` is mounted globally).
### 3. CSS-only tooltips → real Tooltip
Replace the inline `group-hover/*` blocks in `app/routes/settings-models.tsx` (test-error message ~L519, alias list ~L545) with the new `<Tooltip label={...}>` wrapper. Gains keyboard focus + Esc dismiss + collision avoidance.
### 4. SVG-anchored hovers → shared `FloatingTooltip` helper
Two sites anchor to a measured `DOMRect` from SVG/Graphviz output (no wrappable trigger element): `app/routes/run-overview.tsx:303-318` and `app/components/event-debug.tsx:423-432` (+ the thread-DNA one near :639).
Extract a single helper in `app/components/floating-tooltip.tsx`:
```ts
function FloatingTooltip({ rect, placement, children }) // portals to body, applies collision-avoiding style
```
Absorb the logic of `hover-card-style.ts` into it (cover `top`/`bottom` placements). Delete `app/components/hover-card-style.ts`. Both sites use the helper; `run-overview` renders `<StagePopover>` inside.
## Files to modify
Modify:
- `app/components/ui.tsx` — replace Tooltip/HoverCard impls; delete useHoverAnchor
- `app/components/toast.tsx` — shrink to ~30-line sonner shim
- `app/components/toast.test.tsx` — rewrite assertions
- `app/layouts/app-shell.tsx` — mount `TooltipProvider` + sonner `<Toaster />`, drop `<ToastProvider>`
- `app/routes/settings-models.tsx` — swap two inline CSS tooltips for `<Tooltip>`
- `app/routes/run-overview.tsx` — use `FloatingTooltip`
- `app/components/event-debug.tsx` — use `FloatingTooltip` (two call sites)
- `apps/fabro-web/package.json` — add `@radix-ui/react-tooltip`, `@radix-ui/react-hover-card`, `sonner`
Create:
- `app/components/floating-tooltip.tsx`
Delete:
- `app/components/hover-card-style.ts`
## Verification
1. `cd apps/fabro-web && bun run typecheck` — no type errors.
2. `cd apps/fabro-web && bun test``toast.test.tsx` passes against new shim; all other tests unchanged.
3. Run dev locally (`fabro server start` + `cd apps/fabro-web && bun run dev`) and exercise:
- Tooltips: hover the refresh button on `/runs/:id/sandbox/services`, status chip on `/runs/:id/billing`, run-table-row status icons. Confirm hover delay (~200ms shared), Esc dismisses, keyboard focus opens.
- HoverCards: hover stage rows in the stage sidebar, waterfall rows, and the run-detail header chips. Confirm positioning flips near viewport edges (Radix collision detection).
- Toasts: trigger a failed `/runs/:id` action (e.g. retry an unretryable run), confirm red toast persists; trigger a success toast (e.g. archive), confirm auto-dismiss; deep-link to a missing file under `/runs/:id/files/missing-path` — confirm 5s warning.
- SVG hovers: hover Graphviz nodes on `/runs/:id` overview; hover the waterfall event chips in event-debug. Confirm tooltips appear above and clamp to viewport.
4. Lighthouse/axe spot check on settings-models: confirm aliases + test-error tooltips now reachable via keyboard.
## Out of scope
- `ConfirmDialog`, `RowActionsMenu` — already on Headless UI Dialog/Menu, no change.
- `CollapsibleFile` — 40-line one-off, marginal win, leave.
- Theming changes; visual output should match current styling pixel-close.
## Open questions
- Do we want to brand sonner toasts (custom `toastOptions` for color tokens), or accept sonner defaults? Defaults are dark-themed and read well against `bg-panel`, so likely fine.
- `TooltipProvider` `skipDelayDuration` value — 300ms is a sensible default for grouped hovers across a sidebar; revisit if it feels off in use.
## Completed stages
- **toolchain**: succeeded
- 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`
- Output:
```
cargo 1.95.0 (f2d3ce0bd 2026-03-21)
```
- **preflight_compile**: succeeded
- Script: `cargo check -q --workspace 2>&1`
- Output: (empty)
- **preflight_lint**: succeeded
- Script: `cargo +nightly-2026-04-14 clippy -q --workspace --all-targets -- -D warnings 2>&1`
- Output: (empty)
- **implement**: succeeded
- Model: gpt-5.5, 3.3m tokens in / 39.0k out
- **simplify_opus**: succeeded
- Model: claude-opus-4-7, 98.4k tokens in / 40.1k out
- Files: /home/daytona/workspace/fabro/apps/fabro-web/app/components/event-debug.tsx, /home/daytona/workspace/fabro/apps/fabro-web/app/components/floating-tooltip.tsx, /home/daytona/workspace/fabro/apps/fabro-web/app/components/toast.test.tsx, /home/daytona/workspace/fabro/apps/fabro-web/app/components/toast.tsx, /home/daytona/workspace/fabro/apps/fabro-web/app/components/ui.tsx, /home/daytona/workspace/fabro/apps/fabro-web/app/layouts/app-shell.tsx
# Simplify: Code Review and Cleanup
Review changes vs. origin for reuse, quality, and efficiency. Fix any issues found.
## Phase 1: Identify Changes
Run git diff (or git diff HEAD if there are staged changes) to see what changed. If there are no git changes, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
## Phase 2: Launch Three Review Agents in Parallel
Use the Agent tool to launch all three agents concurrently in a single message. Pass each agent the full diff so it has the complete context.
### Agent 1: Code Reuse Review
For each change:
1. Search for existing utilities and helpers that could replace newly written code. Use Grep to find similar patterns elsewhere in the codebase — common locations are utility directories, shared modules, and files adjacent to the changed ones.
2. Flag any new function that duplicates existing functionality. Suggest the existing function to use instead.
3. Flag any inline logic that could use an existing utility — hand-rolled string manipulation, manual path handling, custom environment checks, ad-hoc type guards, and similar patterns are common candidates.
Note: This is a greenfield app, so focus on maximizing simplicity and don't worry about changing things to achieve it.
### Agent 2: Code Quality Review
Review the same changes for hacky patterns:
1. Redundant state: state that duplicates existing state, cached values that could be derived, observers/effects that could be direct calls
2. Parameter sprawl: adding new parameters to a function instead of generalizing or restructuring existing ones
3. Copy-paste with slight variation: near-duplicate code blocks that should be unified with a shared abstraction
4. Leaky abstractions: exposing internal details that should be encapsulated, or breaking existing abstraction boundaries
5. Stringly-typed code: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
Note: This is a greenfield app, so be aggressive in optimizing quality.
### Agent 3: Efficiency Review
Review the same changes for efficiency:
1. Unnecessary work: redundant computations, repeated file reads, duplicate network/API calls, N+1 patterns
2. Missed concurrency: independent operations run sequentially when they could run in parallel
3. Hot-path bloat: new blocking work added to startup or per-request/per-render hot paths
4. Unnecessary existence checks: pre-checking file/resource existence before operating (TOCTOU anti-pattern) — operate directly and handle the error
5. Memory: unbounded data structures, missing cleanup, event listener leaks
6. Overly broad operations: reading entire files when only a portion is needed, loading all items when filtering for one
## Phase 3: Fix Issues
Wait for all three agents to complete. Aggregate their findings and fix each issue directly. If a finding is a false positive or not worth addressing, note it and move on — do not argue with the finding, just skip it.
When done, briefly summarize what was fixed (or confirm the code was already clean).

View file

@ -0,0 +1,5 @@
{
"mode": "agent",
"provider": "openai",
"model": "gpt-5.5"
}