diff --git a/src/main/ipc/terminal-preview-output-stream.ts b/src/main/ipc/terminal-preview-output-stream.ts index e88311f105a..af14424cbe2 100644 --- a/src/main/ipc/terminal-preview-output-stream.ts +++ b/src/main/ipc/terminal-preview-output-stream.ts @@ -81,6 +81,9 @@ export class TerminalPreviewOutputStream { } append(data: string, meta?: TerminalPreviewOutputMeta): void { + if (this.isDisposed || this.awaitingReconnect || this.resyncPending) { + return + } if (this.bufferingSnapshot) { this.appendInitial(data, meta) } else { diff --git a/src/main/ipc/terminal-preview.test.ts b/src/main/ipc/terminal-preview.test.ts index 71ccfaeb4a7..396b1d93f7a 100644 --- a/src/main/ipc/terminal-preview.test.ts +++ b/src/main/ipc/terminal-preview.test.ts @@ -301,6 +301,29 @@ describe('registerTerminalPreviewHandlers', () => { expect(runtime.unsubscribeResize).toHaveBeenCalledTimes(1) }) + it('stops batching changed-grid output while an earlier frame drains before resync', async () => { + vi.useFakeTimers() + const runtime = makeRuntime() + registerTerminalPreviewHandlers(runtime as never) + const sender = makeSender() + await handlers.get('terminalPreview:connect')!(eventFor(sender), { ptyId: 'p1' }) + + runtime.listeners[0]!('old') + await vi.advanceTimersByTimeAsync(5) + runtime.resizeListeners[0]!({ cols: 100, rows: 30 }) + expect(sender.send).toHaveBeenCalledTimes(1) + + runtime.listeners[0]!('captured by the replacement snapshot') + expect(vi.getTimerCount()).toBe(0) + + handlers.get('terminalPreview:ack')!(eventFor(sender), { ptyId: 'p1', bytes: 3 }) + expect(sender.send).toHaveBeenLastCalledWith('terminalPreview:data', { + type: 'resync', + ptyId: 'p1' + }) + expect(sender.send).toHaveBeenCalledTimes(2) + }) + it('claims the PTY grid on fit and reports the size actually in effect', async () => { const runtime = makeRuntime() runtime.getTerminalSize.mockReturnValue({ cols: 132, rows: 40 }) diff --git a/src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts b/src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts index 2bec8d2fab2..e933174e87d 100644 --- a/src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts +++ b/src/renderer/src/components/dashboard-popout/preview-grid-claim.test.ts @@ -45,4 +45,38 @@ describe('createPreviewGridClaim', () => { expect(fit).toHaveBeenCalledWith('pty-1', 90, 30) claim.dispose() }) + + it('coalesces a continuous resize burst into one settled fit request', async () => { + vi.useFakeTimers() + const fit = vi.fn(async (_ptyId: string, cols: number, rows: number) => ({ cols, rows })) + Object.assign(window, { api: { terminalPreview: { fit } } }) + const box = document.createElement('div') + const container = document.createElement('div') + const screen = document.createElement('div') + screen.className = 'xterm-screen' + box.appendChild(container) + container.appendChild(screen) + dimension(box, 'clientWidth', 800) + dimension(box, 'clientHeight', 480) + dimension(screen, 'offsetWidth', 800) + dimension(screen, 'offsetHeight', 384) + const claim = createPreviewGridClaim({ + ptyId: 'pty-1', + container, + getTerminal: () => ({ cols: 80, rows: 24 }) as never + }) + + claim.schedule() + for (let step = 1; step <= 10; step += 1) { + await vi.advanceTimersByTimeAsync(100) + dimension(box, 'clientWidth', 800 + step * 20) + claim.schedule() + } + + expect(fit).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(200) + expect(fit).toHaveBeenCalledTimes(1) + expect(fit).toHaveBeenCalledWith('pty-1', 100, 30) + claim.dispose() + }) }) diff --git a/src/renderer/src/components/dashboard-popout/preview-grid-claim.ts b/src/renderer/src/components/dashboard-popout/preview-grid-claim.ts index 79d85781a74..6f145e13ea1 100644 --- a/src/renderer/src/components/dashboard-popout/preview-grid-claim.ts +++ b/src/renderer/src/components/dashboard-popout/preview-grid-claim.ts @@ -70,9 +70,12 @@ export function createPreviewGridClaim(args: { } const schedule = (): void => { - if (timer || disposed) { + if (disposed) { return } + if (timer) { + clearTimeout(timer) + } timer = setTimeout(() => { timer = null request()