From dd1b28ba2e8d811f47751fa4418407f83978d85a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 23:06:13 -0700 Subject: [PATCH] fix: keep codex redraws off pty batch path (#3992) --- src/main/ipc/pty.test.ts | 34 ++++++++++++++++++++++++++++++++++ src/main/ipc/pty.ts | 13 ++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 4c2026e73..7bf7f1421 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -3266,6 +3266,40 @@ describe('registerPtyHandlers', () => { } }) + it('sends larger ANSI redraws immediately after terminal input', async () => { + vi.useFakeTimers() + const mockProc = createMockProc() + spawnMock.mockReturnValue(mockProc.proc) + + try { + registerPtyHandlers(mainWindow as never) + const spawnResult = (await handlers.get('pty:spawn')!(null, { + cols: 80, + rows: 24, + cwd: '/tmp' + })) as { id: string } + const writeListener = getPtyWriteListener() + + writeListener(null, { + id: spawnResult.id, + data: 'a' + }) + mainWindow.webContents.send.mockClear() + + const redraw = `\x1b[2J\x1b[H${'codex composer redraw '.repeat(80)}` + mockProc.emitData(redraw) + + expect(mainWindow.webContents.send).toHaveBeenCalledWith('pty:data', { + id: spawnResult.id, + data: redraw + }) + vi.advanceTimersByTime(8) + expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1) + } finally { + vi.useRealTimers() + } + }) + it('batches combined pending output that exceeds the interactive size limit', async () => { vi.useFakeTimers() const mockProc = createMockProc() diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index 059ecc082..33e71e71e 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -946,12 +946,23 @@ export function registerPtyHandlers( // large output and non-interactive output must still use the batcher. const INTERACTIVE_OUTPUT_WINDOW_MS = 100 const INTERACTIVE_OUTPUT_MAX_CHARS = 1024 + const INTERACTIVE_REDRAW_MAX_CHARS = PTY_BATCH_FLUSH_CHUNK_CHARS const BACKGROUND_OUTPUT_INPUT_QUIET_MS = 50 const BACKGROUND_OUTPUT_MAX_INPUT_HOLD_MS = 250 let lastRendererInputAt = Number.NEGATIVE_INFINITY let lastRendererInputPtyId: string | null = null let backgroundFlushHeldSince: number | null = null + function isLikelyInteractiveRedraw(data: string): boolean { + if (data.length <= INTERACTIVE_OUTPUT_MAX_CHARS) { + return true + } + // Why: Codex-style TUIs can repaint more than 1 KB per keypress. ANSI + // control redraws are still latency-sensitive, while plain command output + // should stay on the throughput batch path. + return data.length <= INTERACTIVE_REDRAW_MAX_CHARS && data.includes('\x1b[') + } + function getChunkStartSeq(endSeq: number | undefined, data: string): number | undefined { return typeof endSeq === 'number' ? Math.max(0, endSeq - data.length) : undefined } @@ -1176,7 +1187,7 @@ export function registerPtyHandlers( const nextData = pending.data const lastInputAt = lastInputAtByPty.get(payload.id) const isInteractiveOutput = - nextData.length <= INTERACTIVE_OUTPUT_MAX_CHARS && + isLikelyInteractiveRedraw(nextData) && lastInputAt !== undefined && performance.now() - lastInputAt <= INTERACTIVE_OUTPUT_WINDOW_MS if (isInteractiveOutput) {