diff --git a/.github/workflows/react-doctor.yml b/.github/workflows/react-doctor.yml index b9076bcb8a..23e0a4777a 100644 --- a/.github/workflows/react-doctor.yml +++ b/.github/workflows/react-doctor.yml @@ -50,7 +50,7 @@ jobs: directory: gui # Pin the npm engine — the action wrapper would otherwise fetch # react-doctor@latest, silently skewing CI from the local pinned runs. - version: "0.9.2" + version: "0.9.3" # Fail the job on any finding (errors or warnings). blocking: warning comment: false diff --git a/gui/README.md b/gui/README.md index 8981908b95..d46ca34a5e 100644 --- a/gui/README.md +++ b/gui/README.md @@ -49,6 +49,6 @@ bun run setup:hooks # pre-push runs doctor when gui/ changed | Tool | Role | |------|------| | **ESLint** (`bun run lint`) | Hard gate in CI and expected before merge | -| **React Doctor** (`bun run doctor`) | Gating React health check pinned to react-doctor 0.9.2 (`blocking: warning`). Pre-push runs it only if `gui/` changed and fails the push on findings. The CI workflow fails the job on any finding | +| **React Doctor** (`bun run doctor`) | Gating React health check pinned to react-doctor 0.9.3 (`blocking: warning`). Pre-push runs it only if `gui/` changed and fails the push on findings. The CI workflow fails the job on any finding | Fix ESLint errors first. Use `doctor` / `doctor:full` for deeper React triage. diff --git a/gui/package.json b/gui/package.json index ce7f602b0b..6655ff8ac7 100644 --- a/gui/package.json +++ b/gui/package.json @@ -9,8 +9,8 @@ "lint": "eslint .", "test": "bun test tests", "lint:i18n": "eslint src/pages src/components src/App.tsx src/ui.tsx", - "doctor": "npx --yes react-doctor@0.9.2 --verbose --scope changed --base origin/main --no-telemetry", - "doctor:full": "npx --yes react-doctor@0.9.2 --verbose --scope full --no-telemetry", + "doctor": "npx --yes react-doctor@0.9.3 --verbose --scope changed --base origin/main --no-telemetry", + "doctor:full": "npx --yes react-doctor@0.9.3 --verbose --scope full --no-telemetry", "preview": "vite preview" }, "dependencies": { diff --git a/gui/src/components/data-surface.tsx b/gui/src/components/data-surface.tsx index b19687a67f..f42867b5fd 100644 --- a/gui/src/components/data-surface.tsx +++ b/gui/src/components/data-surface.tsx @@ -13,7 +13,7 @@ import type { CSSProperties, ReactNode } from "react"; * Lets a page mirror its ready geometry without exposing placeholder values to assistive * technology. The surrounding skeleton owns the single announced sentence. */ -export function DataSurfaceSkeletonBlock({ +function DataSurfaceSkeletonBlock({ className, style, }: { diff --git a/gui/src/hooks/useCodexAccountPool.ts b/gui/src/hooks/useCodexAccountPool.ts index 3f3752fcb3..9f610f9d60 100644 --- a/gui/src/hooks/useCodexAccountPool.ts +++ b/gui/src/hooks/useCodexAccountPool.ts @@ -110,7 +110,8 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou // Pause leases live in a ref: pausing must not re-render, and the effect below reads // the live set rather than a captured snapshot. const [pauseCount, setPauseCount] = useState(0); - const pauseTokensRef = useRef>(new Set()); + const pauseTokensRef = useRef | null>(null); + if (pauseTokensRef.current === null) pauseTokensRef.current = new Set(); // Which apiBase this instance has already kicked its initial load for. StrictMode double-invokes // the mount effect, and the deferred load is deliberately uncancellable, so the guard has to live // here rather than in the effect's cleanup. @@ -119,7 +120,8 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou // Set by switchAccount so a background load already in flight cannot roll the active // id back to a value the server had not yet committed when that request was issued. const pendingActiveIdRef = useRef<{ id: string | null } | null>(null); - const observersRef = useRef>(new Set()); + const observersRef = useRef | null>(null); + if (observersRef.current === null) observersRef.current = new Set(); // Last /active payload an actual read returned. Surfaces that mount after a // load already finished read it to seed their UI instead of waiting a poll interval. const lastActiveRef = useRef<{ value: unknown } | null>(null); @@ -131,13 +133,13 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou const pauseMutationRef = useRef<"bulk" | { accountId: string } | null>(null); const subscribeLoadObserver = useCallback((observer: CodexAccountLoadObserver) => { - observersRef.current.add(observer); + observersRef.current!.add(observer); // Subscribing stays silent. `acceptActiveRead` means "a read that started at this // revision came back", and useCodexAutoSwitch / CodexPoolStrategySetting decide their // editing and saving disposition from that. Synthesising one on subscribe can overwrite // an in-flight draft or arm a spurious post-save refresh. Late surfaces seed themselves // from readLastThreshold()/readLastActive(), which apply only while uninitialized. - return () => { observersRef.current.delete(observer); }; + return () => { observersRef.current!.delete(observer); }; }, []); /** Last threshold an actual read returned, or undefined when none has succeeded yet. */ @@ -156,7 +158,7 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou // observer snapshot below cannot leave the counter stuck above zero. try { // Snapshot subscribers so an unsubscribe mid-flight cannot desync begin/accept pairs. - const observers = [...observersRef.current]; + const observers = [...observersRef.current!]; const revisions = new Map(); for (const observer of observers) revisions.set(observer, observer.beginActiveRead()); // Soft refresh when boxes are already on screen — avoid full-page loading flash. @@ -282,14 +284,14 @@ export function useCodexAccountPool(apiBase: string, enabled = true): CodexAccou const pauseRefresh = useCallback((): PauseToken => { const token = {} as PauseToken; - pauseTokensRef.current.add(token); - setPauseCount(pauseTokensRef.current.size); + pauseTokensRef.current!.add(token); + setPauseCount(pauseTokensRef.current!.size); return token; }, []); const resumeRefresh = useCallback((token: PauseToken) => { - if (!pauseTokensRef.current.delete(token)) return; - setPauseCount(pauseTokensRef.current.size); + if (!pauseTokensRef.current!.delete(token)) return; + setPauseCount(pauseTokensRef.current!.size); }, []); const switchAccount = useCallback(async (id: string | null) => { diff --git a/gui/src/icons.tsx b/gui/src/icons.tsx index d8e4b1dae9..bed8492599 100644 --- a/gui/src/icons.tsx +++ b/gui/src/icons.tsx @@ -43,7 +43,6 @@ export const IconSun = (p: P) => ( export const IconMoon = (p: P) => (); export const IconMonitor = (p: P) => (); export const IconGlobe = (p: P) => (); -export const IconSparkle = (p: P) => (); /** Crossed arrows — Combos workspace nav / rail marker (load-balance / hop). */ export const IconShuffle = (p: P) => ( diff --git a/gui/src/pages/Integrations.tsx b/gui/src/pages/Integrations.tsx index c2f4aceffa..772598af26 100644 --- a/gui/src/pages/Integrations.tsx +++ b/gui/src/pages/Integrations.tsx @@ -72,7 +72,8 @@ export default function Integrations({ apiBase }: { apiBase: string }) { const [mounted, setMounted] = useState>( () => new Set([readIntegrationTab()]), ); - const tabRefs = useRef(new Map()); + const tabRefs = useRef | null>(null); + if (tabRefs.current === null) tabRefs.current = new Map(); /* * Every tab change goes through here, whether it came from a click or from @@ -102,7 +103,7 @@ export default function Integrations({ apiBase }: { apiBase: string }) { activateTab(next); if (moveFocus) { window.requestAnimationFrame(() => { - tabRefs.current.get(next)?.focus({ preventScroll: true }); + tabRefs.current!.get(next)?.focus({ preventScroll: true }); }); } }; @@ -131,8 +132,8 @@ export default function Integrations({ apiBase }: { apiBase: string }) {