feat: Batch dev server logs and fix React module resolution order

This commit is contained in:
gsxdsm
2026-03-02 23:19:17 -08:00
parent 90be17fd79
commit 736e12d397
3 changed files with 28 additions and 20 deletions

View File

@@ -74,6 +74,20 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
// Keep track of whether we've fetched initial logs // Keep track of whether we've fetched initial logs
const hasFetchedInitialLogs = useRef(false); 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<number | null>(null);
const resetPendingOutput = useCallback(() => {
if (rafIdRef.current !== null) {
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
pendingOutputRef.current = '';
}, []);
/** /**
* Fetch buffered logs from the server * Fetch buffered logs from the server
*/ */
@@ -130,6 +144,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
* Clear logs and reset state * Clear logs and reset state
*/ */
const clearLogs = useCallback(() => { const clearLogs = useCallback(() => {
resetPendingOutput();
setState({ setState({
logs: '', logs: '',
logsVersion: 0, logsVersion: 0,
@@ -144,13 +159,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
serverError: null, serverError: null,
}); });
hasFetchedInitialLogs.current = false; hasFetchedInitialLogs.current = false;
}, []); }, [resetPendingOutput]);
// 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<number | null>(null);
const flushPendingOutput = useCallback(() => { const flushPendingOutput = useCallback(() => {
rafIdRef.current = null; 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 // Clean up pending RAF on unmount to prevent state updates after unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
if (rafIdRef.current !== null) { resetPendingOutput();
cancelAnimationFrame(rafIdRef.current);
rafIdRef.current = null;
}
}; };
}, []); }, [resetPendingOutput]);
// Fetch initial logs when worktreePath changes // Fetch initial logs when worktreePath changes
useEffect(() => { useEffect(() => {
@@ -230,6 +236,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
switch (event.type) { switch (event.type) {
case 'dev-server:started': { case 'dev-server:started': {
resetPendingOutput();
const { payload } = event; const { payload } = event;
logger.info('Dev server started:', payload); logger.info('Dev server started:', payload);
setState((prev) => ({ setState((prev) => ({
@@ -279,7 +286,7 @@ export function useDevServerLogs({ worktreePath, autoSubscribe = true }: UseDevS
}); });
return unsubscribe; return unsubscribe;
}, [worktreePath, autoSubscribe, appendLogs]); }, [worktreePath, autoSubscribe, appendLogs, resetPendingOutput]);
return { return {
...state, ...state,

View File

@@ -930,7 +930,8 @@ export class HttpApiClient implements ElectronAPI {
const isHighFrequency = const isHighFrequency =
data.type === 'dev-server:output' || data.type === 'dev-server:output' ||
data.type === 'test-runner: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) { if (!isHighFrequency) {
logger.info('WebSocket message:', data.type); logger.info('WebSocket message:', data.type);
} }

View File

@@ -248,16 +248,12 @@ export default defineConfig(({ command }) => {
{ find: '@', replacement: path.resolve(__dirname, './src') }, { find: '@', replacement: path.resolve(__dirname, './src') },
// Force ALL React imports (including from nested deps like zustand@4 inside // Force ALL React imports (including from nested deps like zustand@4 inside
// @xyflow/react) to resolve to a single copy. // @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(\/|$)/, find: /^react-dom(\/|$)/,
replacement: path.resolve(__dirname, '../../node_modules/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', find: 'react/jsx-runtime',
replacement: path.resolve(__dirname, '../../node_modules/react/jsx-runtime.js'), replacement: path.resolve(__dirname, '../../node_modules/react/jsx-runtime.js'),
@@ -266,6 +262,10 @@ export default defineConfig(({ command }) => {
find: 'react/jsx-dev-runtime', find: 'react/jsx-dev-runtime',
replacement: path.resolve(__dirname, '../../node_modules/react/jsx-dev-runtime.js'), 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'], dedupe: ['react', 'react-dom', 'zustand', 'use-sync-external-store', '@xyflow/react'],
}, },