mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +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)
130 lines
3.6 KiB
TypeScript
130 lines
3.6 KiB
TypeScript
import type {
|
|
ChatModelAdapter,
|
|
ChatModelRunResult,
|
|
ThreadAssistantMessagePart,
|
|
ThreadMessageLike,
|
|
} from "@assistant-ui/react";
|
|
|
|
import type { Chat, ChatContentPart, ChatMessage } from "./chats-types";
|
|
import { pickReply } from "./chats-script";
|
|
|
|
const STREAM_CHUNK_CHARS = 28;
|
|
const STREAM_CHUNK_INTERVAL_MS = 55;
|
|
|
|
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
if (signal.aborted) {
|
|
reject(new DOMException("Aborted", "AbortError"));
|
|
return;
|
|
}
|
|
const handle = setTimeout(resolve, ms);
|
|
signal.addEventListener(
|
|
"abort",
|
|
() => {
|
|
clearTimeout(handle);
|
|
reject(new DOMException("Aborted", "AbortError"));
|
|
},
|
|
{ once: true },
|
|
);
|
|
});
|
|
}
|
|
|
|
function toAssistantParts(
|
|
content: readonly ChatContentPart[],
|
|
): ThreadAssistantMessagePart[] {
|
|
const out: ThreadAssistantMessagePart[] = [];
|
|
for (const part of content) {
|
|
if (part.kind === "text") {
|
|
out.push({ type: "text", text: part.data.text });
|
|
} else if (part.kind === "tool_call") {
|
|
out.push({
|
|
type: "tool-call",
|
|
toolCallId: part.data.tool_call_id,
|
|
toolName: part.data.name,
|
|
args: part.data.arguments,
|
|
argsText: JSON.stringify(part.data.arguments),
|
|
});
|
|
} else if (part.kind === "tool_result") {
|
|
for (let i = out.length - 1; i >= 0; i--) {
|
|
const candidate = out[i];
|
|
if (
|
|
candidate?.type === "tool-call" &&
|
|
candidate.toolCallId === part.data.tool_call_id
|
|
) {
|
|
out[i] = { ...candidate, result: part.data.content };
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function createScriptedAdapter(args: {
|
|
getChat: () => Chat | undefined;
|
|
onReplyComplete: (reply: ChatMessage) => void;
|
|
}): ChatModelAdapter {
|
|
return {
|
|
async *run({ abortSignal }) {
|
|
const chat = args.getChat();
|
|
const reply = pickReply(chat?.scriptIndex ?? 0);
|
|
const accumulated: ChatContentPart[] = [];
|
|
|
|
for (const part of reply.content) {
|
|
if (part.kind === "text") {
|
|
const text = part.data.text;
|
|
let cursor = 0;
|
|
accumulated.push({ kind: "text", data: { text: "" } });
|
|
const accIndex = accumulated.length - 1;
|
|
while (cursor < text.length) {
|
|
cursor = Math.min(cursor + STREAM_CHUNK_CHARS, text.length);
|
|
accumulated[accIndex] = {
|
|
kind: "text",
|
|
data: { text: text.slice(0, cursor) },
|
|
};
|
|
yield buildUpdate(accumulated);
|
|
if (cursor < text.length) {
|
|
await sleep(STREAM_CHUNK_INTERVAL_MS, abortSignal);
|
|
}
|
|
}
|
|
} else {
|
|
accumulated.push(part);
|
|
yield buildUpdate(accumulated);
|
|
await sleep(STREAM_CHUNK_INTERVAL_MS * 3, abortSignal);
|
|
}
|
|
}
|
|
|
|
args.onReplyComplete(reply);
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildUpdate(parts: ChatContentPart[]): ChatModelRunResult {
|
|
return { content: toAssistantParts(parts) };
|
|
}
|
|
|
|
export function toThreadMessages(
|
|
messages: readonly ChatMessage[],
|
|
): ThreadMessageLike[] {
|
|
return messages.map((msg) => {
|
|
if (msg.role === "user") {
|
|
const content = [];
|
|
for (const part of msg.content) {
|
|
if (part.kind === "text") {
|
|
content.push({ type: "text", text: part.data.text } as const);
|
|
}
|
|
}
|
|
return {
|
|
role: "user",
|
|
content,
|
|
};
|
|
}
|
|
if (msg.role === "assistant") {
|
|
return {
|
|
role: "assistant",
|
|
content: toAssistantParts(msg.content),
|
|
};
|
|
}
|
|
return { role: "system", content: [] };
|
|
});
|
|
}
|