diff --git a/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts b/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts index 21d065dc50..68be93bd8c 100644 --- a/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts +++ b/src/web-ui/src/features/dispatch/DispatchJobObserver.test.ts @@ -845,7 +845,7 @@ describe('DispatchJobObserver', () => { cleanup(); }); - it('continues polling while hidden so background notifications can observe changes', async () => { + it('pauses polling while hidden to reduce background CPU usage', async () => { registerRunningJob(); mocks.status.mockResolvedValue(status({ state: 'running' })); Object.defineProperty(document, 'visibilityState', { @@ -855,7 +855,7 @@ describe('DispatchJobObserver', () => { const cleanup = installDispatchJobObserver(createContext()); await vi.advanceTimersByTimeAsync(0); - expect(mocks.status).toHaveBeenCalledTimes(1); + expect(mocks.status).not.toHaveBeenCalled(); cleanup(); Object.defineProperty(document, 'visibilityState', { @@ -864,6 +864,30 @@ describe('DispatchJobObserver', () => { }); }); + it('resumes polling when the window becomes visible again', async () => { + registerRunningJob(); + mocks.status.mockResolvedValue(status({ state: 'running' })); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'hidden', + }); + const cleanup = installDispatchJobObserver(createContext()); + + await vi.advanceTimersByTimeAsync(0); + expect(mocks.status).not.toHaveBeenCalled(); + + Object.defineProperty(document, 'visibilityState', { + configurable: true, + value: 'visible', + }); + document.dispatchEvent(new Event('visibilitychange')); + + await vi.advanceTimersByTimeAsync(0); + expect(mocks.status).toHaveBeenCalled(); + + cleanup(); + }); + it('does not present target cancellation as a successful completion', async () => { registerRunningJob(); const context = createContext(); diff --git a/src/web-ui/src/features/dispatch/DispatchJobObserver.ts b/src/web-ui/src/features/dispatch/DispatchJobObserver.ts index f86fa3660d..8d7736db5e 100644 --- a/src/web-ui/src/features/dispatch/DispatchJobObserver.ts +++ b/src/web-ui/src/features/dispatch/DispatchJobObserver.ts @@ -998,6 +998,7 @@ export function installDispatchJobObserver(context: FlowChatContext): () => void async function run(requestedJobId?: string): Promise { if (!ownsLease() || isPeerDeviceModeActive()) return; + if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; if (inFlight) { queuedJobId = requestedJobId; return; @@ -1095,8 +1096,19 @@ export function installDispatchJobObserver(context: FlowChatContext): () => void void run(); }, DISPATCH_JOB_POLL_INTERVAL_MS); handleVisibilityChanged = () => { - if (typeof document === 'undefined' || document.visibilityState === 'visible') { + if (typeof document === 'undefined') return; + if (document.visibilityState === 'visible') { + if (interval === null) { + interval = setInterval(() => { + void run(); + }, DISPATCH_JOB_POLL_INTERVAL_MS); + } schedule(); + } else { + if (interval !== null) { + clearInterval(interval); + interval = null; + } } }; if (typeof document !== 'undefined') { diff --git a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx index 189705e19d..df99c068eb 100644 --- a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx +++ b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.tsx @@ -178,6 +178,7 @@ export const BackgroundCommandOutputPanel: React.FC = ({ } const tick = () => { + if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; void checkFileModification(); }; const pollOffsetMs = getPollOffsetMs(filePath); diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx index af36da6b9f..c9ecf0a692 100644 --- a/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx +++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.tsx @@ -414,6 +414,7 @@ const MarkdownEditor: React.FC = ({ return; } const tick = () => { + if (typeof document !== 'undefined' && document.visibilityState === 'hidden') return; void checkMarkdownDisk(); }; const pollOffsetMs = getPollOffsetMs(filePath);