mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +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)
142 lines
3.8 KiB
TypeScript
142 lines
3.8 KiB
TypeScript
import {
|
|
SessionsApiAxiosParamCreator,
|
|
type EventEnvelope,
|
|
type SubmitTurnRequest,
|
|
} from "@qltysh/fabro-api-client";
|
|
|
|
import {
|
|
apiErrorFromFetchResponse,
|
|
generatedApiConfiguration,
|
|
} from "./api-client";
|
|
|
|
export type SessionStreamEvent = EventEnvelope;
|
|
|
|
type FetchLike = (
|
|
input: string,
|
|
init?: RequestInit,
|
|
) => Promise<Response>;
|
|
|
|
interface SessionStreamOptions {
|
|
sessionId: string;
|
|
signal?: AbortSignal;
|
|
fetchImpl?: FetchLike;
|
|
onEvent: (event: SessionStreamEvent) => void;
|
|
}
|
|
|
|
export interface StreamSessionTurnOptions extends SessionStreamOptions {
|
|
input: string;
|
|
turnId?: string;
|
|
}
|
|
|
|
export interface StreamSessionTurnResult {
|
|
turnId: string | null;
|
|
}
|
|
|
|
export interface AttachSessionEventsOptions extends SessionStreamOptions {
|
|
sinceSeq?: number;
|
|
}
|
|
|
|
export async function streamSessionTurn({
|
|
sessionId,
|
|
input,
|
|
turnId,
|
|
signal,
|
|
fetchImpl = fetch,
|
|
onEvent,
|
|
}: StreamSessionTurnOptions): Promise<StreamSessionTurnResult> {
|
|
const body: SubmitTurnRequest = { input };
|
|
if (turnId) body.turn_id = turnId;
|
|
|
|
const request = await SessionsApiAxiosParamCreator(
|
|
generatedApiConfiguration,
|
|
).submitSessionTurn(sessionId, body, { signal });
|
|
const response = await fetchImpl(request.url, fetchInitFromAxiosRequest(request.options));
|
|
await throwIfApiError(response);
|
|
|
|
await readEventStream(response, onEvent);
|
|
return { turnId: response.headers.get("x-fabro-turn-id") };
|
|
}
|
|
|
|
export async function attachSessionEvents({
|
|
sessionId,
|
|
sinceSeq,
|
|
signal,
|
|
fetchImpl = fetch,
|
|
onEvent,
|
|
}: AttachSessionEventsOptions): Promise<void> {
|
|
const request = await SessionsApiAxiosParamCreator(
|
|
generatedApiConfiguration,
|
|
).attachSessionEvents(sessionId, sinceSeq, { signal });
|
|
const response = await fetchImpl(request.url, fetchInitFromAxiosRequest(request.options));
|
|
await throwIfApiError(response);
|
|
|
|
await readEventStream(response, onEvent);
|
|
}
|
|
|
|
async function throwIfApiError(response: Response): Promise<void> {
|
|
const error = await apiErrorFromFetchResponse(response);
|
|
if (error) throw error;
|
|
}
|
|
|
|
function fetchInitFromAxiosRequest(options: {
|
|
method?: string;
|
|
headers?: unknown;
|
|
data?: unknown;
|
|
signal?: unknown;
|
|
}): RequestInit {
|
|
const init: RequestInit = {
|
|
method: options.method,
|
|
credentials: "same-origin",
|
|
headers: options.headers as HeadersInit,
|
|
signal: options.signal as AbortSignal | undefined,
|
|
};
|
|
if (options.data !== undefined) {
|
|
init.body = typeof options.data === "string"
|
|
? options.data
|
|
: JSON.stringify(options.data);
|
|
}
|
|
return init;
|
|
}
|
|
|
|
async function readEventStream(
|
|
response: Response,
|
|
onEvent: (event: SessionStreamEvent) => void,
|
|
): Promise<void> {
|
|
if (!response.body) return;
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = "";
|
|
|
|
while (true) {
|
|
// react-doctor-disable-next-line react-doctor/async-await-in-loop -- Streaming readers must consume chunks sequentially to preserve SSE order and decoder state.
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
buffer = drainSseBuffer(buffer, onEvent);
|
|
}
|
|
|
|
buffer += decoder.decode();
|
|
drainSseBuffer(`${buffer}\n\n`, onEvent);
|
|
}
|
|
|
|
function drainSseBuffer(
|
|
buffer: string,
|
|
onEvent: (event: SessionStreamEvent) => void,
|
|
): string {
|
|
let cursor = 0;
|
|
while (true) {
|
|
const match = /\r?\n\r?\n/g.exec(buffer.slice(cursor));
|
|
if (!match) return buffer.slice(cursor);
|
|
const next = cursor + match.index;
|
|
const frame = buffer.slice(cursor, next);
|
|
cursor = next + match[0].length;
|
|
const data = frame
|
|
.split(/\r?\n/)
|
|
.filter((line) => line.startsWith("data:"))
|
|
.map((line) => line.slice("data:".length).trimStart())
|
|
.join("\n");
|
|
if (!data) continue;
|
|
onEvent(JSON.parse(data) as SessionStreamEvent);
|
|
}
|
|
}
|