mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
## 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)
157 lines
5 KiB
TypeScript
157 lines
5 KiB
TypeScript
import { useReducer, useRef } from "react";
|
|
import { PencilIcon } from "@heroicons/react/16/solid";
|
|
|
|
import { ApiError } from "../lib/api-client";
|
|
import { useUpdateRunTitle } from "../lib/mutations";
|
|
import { InlineMarkdown } from "./inline-markdown";
|
|
import { useToast } from "./toast";
|
|
|
|
const TITLE_MAX_LENGTH = 100;
|
|
|
|
type EditState =
|
|
| { kind: "view" }
|
|
| { kind: "editing"; draft: string; submitted: boolean };
|
|
|
|
type EditAction =
|
|
| { type: "start"; title: string }
|
|
| { type: "change"; draft: string }
|
|
| { type: "mark_submitted" }
|
|
| { type: "allow_retry" }
|
|
| { type: "cancel" };
|
|
|
|
function editReducer(state: EditState, action: EditAction): EditState {
|
|
switch (action.type) {
|
|
case "start":
|
|
return { kind: "editing", draft: action.title, submitted: false };
|
|
case "change":
|
|
return state.kind === "editing"
|
|
? { ...state, draft: action.draft }
|
|
: state;
|
|
case "mark_submitted":
|
|
return state.kind === "editing"
|
|
? { ...state, submitted: true }
|
|
: state;
|
|
case "allow_retry":
|
|
return state.kind === "editing"
|
|
? { ...state, submitted: false }
|
|
: state;
|
|
case "cancel":
|
|
return { kind: "view" };
|
|
}
|
|
}
|
|
|
|
function focusInputNextFrame(callback: () => void): void {
|
|
if (typeof requestAnimationFrame === "function") {
|
|
requestAnimationFrame(callback);
|
|
} else {
|
|
setTimeout(callback, 0);
|
|
}
|
|
}
|
|
|
|
export function EditableRunTitle({ runId, title }: { runId: string; title: string }) {
|
|
const [editState, dispatchEdit] = useReducer(editReducer, { kind: "view" });
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const updateMutation = useUpdateRunTitle(runId);
|
|
const { push } = useToast();
|
|
const isSaving = updateMutation.isMutating;
|
|
const isEditing = editState.kind === "editing";
|
|
const draft = isEditing ? editState.draft : "";
|
|
|
|
const enterEdit = () => {
|
|
dispatchEdit({ type: "start", title });
|
|
focusInputNextFrame(() => {
|
|
inputRef.current?.focus();
|
|
inputRef.current?.select();
|
|
});
|
|
};
|
|
|
|
const exitEdit = () => {
|
|
dispatchEdit({ type: "cancel" });
|
|
};
|
|
|
|
const submit = async () => {
|
|
if (editState.kind !== "editing" || editState.submitted) return;
|
|
const trimmed = draft.trim();
|
|
if (trimmed === title.trim()) {
|
|
exitEdit();
|
|
return;
|
|
}
|
|
if (trimmed.length === 0) {
|
|
push({ message: "Run title can't be blank.", tone: "error" });
|
|
inputRef.current?.focus();
|
|
return;
|
|
}
|
|
dispatchEdit({ type: "mark_submitted" });
|
|
try {
|
|
await updateMutation.trigger({ title: trimmed });
|
|
exitEdit();
|
|
push({ message: "Run title updated." });
|
|
} catch (error) {
|
|
dispatchEdit({ type: "allow_retry" });
|
|
const message = error instanceof ApiError && error.message
|
|
? error.message
|
|
: "Could not update run title.";
|
|
push({ message, tone: "error" });
|
|
focusInputNextFrame(() => inputRef.current?.focus());
|
|
}
|
|
};
|
|
|
|
if (isEditing) {
|
|
const remaining = TITLE_MAX_LENGTH - draft.length;
|
|
const showCount = remaining <= 20;
|
|
return (
|
|
<div className="min-w-0">
|
|
<input
|
|
ref={inputRef}
|
|
name="run-title"
|
|
aria-label="Run title"
|
|
type="text"
|
|
value={draft}
|
|
maxLength={TITLE_MAX_LENGTH}
|
|
disabled={isSaving}
|
|
onChange={(e) => dispatchEdit({ type: "change", draft: e.target.value })}
|
|
onBlur={() => void submit()}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
void submit();
|
|
} else if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
exitEdit();
|
|
}
|
|
}}
|
|
className="-mx-2 block w-full rounded-md bg-panel-alt px-2 py-0.5 text-xl font-semibold text-fg outline-1 -outline-offset-1 outline-line-strong focus:outline-2 focus:-outline-offset-1 focus:outline-teal-500 disabled:opacity-60"
|
|
/>
|
|
<p className="mt-1.5 flex items-center gap-2 text-xs text-fg-muted">
|
|
<span>
|
|
{isSaving ? "Saving…" : "Press Enter to save · Esc to cancel"}
|
|
</span>
|
|
{showCount && !isSaving && (
|
|
<span className={remaining < 0 ? "text-coral" : "tabular-nums"}>
|
|
{remaining} left
|
|
</span>
|
|
)}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<h2 className="text-xl font-semibold text-fg">
|
|
<button
|
|
type="button"
|
|
onClick={enterEdit}
|
|
aria-label="Edit run title"
|
|
className="group/title -mx-2 flex min-w-0 max-w-full items-center gap-1.5 rounded-md px-2 py-0.5 text-left text-fg transition-colors hover:bg-overlay focus-visible:bg-overlay focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500"
|
|
>
|
|
<span className="min-w-0 truncate">
|
|
<InlineMarkdown content={title} />
|
|
</span>
|
|
<PencilIcon
|
|
aria-hidden="true"
|
|
className="size-3.5 shrink-0 text-fg-muted opacity-0 transition-opacity group-hover/title:opacity-100 group-focus-visible/title:opacity-100"
|
|
/>
|
|
</button>
|
|
</h2>
|
|
);
|
|
}
|