From 736e12d39700546b126ee4b4f34ac0b81c059312 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 2 Mar 2026 23:19:17 -0800 Subject: [PATCH] feat: Batch dev server logs and fix React module resolution order --- .../hooks/use-dev-server-logs.ts | 33 +++++++++++-------- apps/ui/src/lib/http-api-client.ts | 3 +- apps/ui/vite.config.mts | 12 +++---- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-server-logs.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-server-logs.ts index cf54aa6a..6461e851 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-server-logs.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-dev-server-logs.ts @@ -74,6 +74,20 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS // Keep track of whether we've fetched initial logs const hasFetchedInitialLogs = useRef(false); + // Buffer for batching rapid output events into fewer setState calls. + // Content accumulates here and is flushed via requestAnimationFrame, + // ensuring at most one React re-render per animation frame (~60fps max). + const pendingOutputRef = useRef(''); + const rafIdRef = useRef(null); + + const resetPendingOutput = useCallback(() => { + if (rafIdRef.current !== null) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } + pendingOutputRef.current = ''; + }, []); + /** * Fetch buffered logs from the server */ @@ -130,6 +144,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS * Clear logs and reset state */ const clearLogs = useCallback(() => { + resetPendingOutput(); setState({ logs: '', logsVersion: 0, @@ -144,13 +159,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS serverError: null, }); hasFetchedInitialLogs.current = false; - }, []); - - // Buffer for batching rapid output events into fewer setState calls. - // Content accumulates here and is flushed via requestAnimationFrame, - // ensuring at most one React re-render per animation frame (~60fps max). - const pendingOutputRef = useRef(''); - const rafIdRef = useRef(null); + }, [resetPendingOutput]); const flushPendingOutput = useCallback(() => { rafIdRef.current = null; @@ -197,12 +206,9 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS // Clean up pending RAF on unmount to prevent state updates after unmount useEffect(() => { return () => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; - } + resetPendingOutput(); }; - }, []); + }, [resetPendingOutput]); // Fetch initial logs when worktreePath changes useEffect(() => { @@ -230,6 +236,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS switch (event.type) { case 'dev-server:started': { + resetPendingOutput(); const { payload } = event; logger.info('Dev server started:', payload); setState((prev) => ({ @@ -279,7 +286,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS }); return unsubscribe; - }, [worktreePath, autoSubscribe, appendLogs]); + }, [worktreePath, autoSubscribe, appendLogs, resetPendingOutput]); return { ...state, diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index efaf92e3..e92ab311 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -930,7 +930,8 @@ export class HttpApiClient implements ElectronAPI { const isHighFrequency = data.type === 'dev-server:output' || data.type === 'test-runner:output' || - data.type === 'auto_mode_progress'; + data.type === 'feature:progress' || + (data.type === 'auto-mode:event' && data.payload?.type === 'auto_mode_progress'); if (!isHighFrequency) { logger.info('WebSocket message:', data.type); } diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts index ab4f9134..a24caefa 100644 --- a/apps/ui/vite.config.mts +++ b/apps/ui/vite.config.mts @@ -248,16 +248,12 @@ export default defineConfig(({ command }) => { { find: '@', replacement: path.resolve(__dirname, './src') }, // Force ALL React imports (including from nested deps like zustand@4 inside // @xyflow/react) to resolve to a single copy. + // Explicit subpath aliases must come BEFORE the broad regex so Vite's + // first-match-wins resolution applies the specific match first. { find: /^react-dom(\/|$)/, replacement: path.resolve(__dirname, '../../node_modules/react-dom') + '/', }, - { - find: /^react(\/|$)/, - replacement: path.resolve(__dirname, '../../node_modules/react') + '/', - }, - // Explicit subpath aliases avoid mixed module IDs between bare imports and - // optimized deps (e.g. react/jsx-runtime), which can manifest as duplicate React. { find: 'react/jsx-runtime', replacement: path.resolve(__dirname, '../../node_modules/react/jsx-runtime.js'), @@ -266,6 +262,10 @@ export default defineConfig(({ command }) => { find: 'react/jsx-dev-runtime', replacement: path.resolve(__dirname, '../../node_modules/react/jsx-dev-runtime.js'), }, + { + find: /^react(\/|$)/, + replacement: path.resolve(__dirname, '../../node_modules/react') + '/', + }, ], dedupe: ['react', 'react-dom', 'zustand', 'use-sync-external-store', '@xyflow/react'], },