From cdd5706f5b80a3112f5ea36880f32fd4683b0d0b Mon Sep 17 00:00:00 2001 From: Justin Skowyra <289723040+Jstn-1g@users.noreply.github.com> Date: Thu, 27 Aug 2026 07:13:39 -0400 Subject: [PATCH] fix(terminal): contain transient resize failures --- src/agent-pty.ts | 22 ++++++++++++++++++++++ src/index.ts | 8 +++----- tests/agent-pty.spec.ts | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/agent-pty.ts b/src/agent-pty.ts index 7e4030e..c5d8393 100644 --- a/src/agent-pty.ts +++ b/src/agent-pty.ts @@ -40,6 +40,28 @@ export function clampDims(cols: number, rows: number): { cols: number; rows: num return { cols: clamp(cols), rows: clamp(rows) } } +/** + * Best-effort resize for WebSocket-driven terminal views. Layout animation + * can briefly produce unusable dimensions, and node-pty can reject a resize + * after the socket setup's outer try/catch has returned. Ignore that one + * frame so the host stays alive and a later valid measurement can retry. + * Returns whether node-pty accepted the resize. + */ +export function tryResizePty( + pty: Pick, + cols: number, + rows: number, +): boolean { + if (!Number.isFinite(cols) || !Number.isFinite(rows)) return false + const dims = clampDims(cols, rows) + try { + pty.resize(dims.cols, dims.rows) + return true + } catch { + return false + } +} + /** * Serializable snapshot of one agent terminal — the shape the model sees * through `terminal_list` and the sidebar sees through the push endpoint. diff --git a/src/index.ts b/src/index.ts index 46c9481..4808ce6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,7 +41,7 @@ import { launchExternal } from './open-external.ts' import * as git from './git.ts' import { SettingsConflictError, settingsNamespace, type SettingsNamespace } from '@deepseek-ai/dsh-settings' import { defaultShell, ensureSpawnHelper, PtyManager, shellDisplayName } from './pty-manager.ts' -import { AgentPtyRegistry, clampDims, type AgentTerminalHandle } from './agent-pty.ts' +import { AgentPtyRegistry, tryResizePty, type AgentTerminalHandle } from './agent-pty.ts' import { DSH_NODE_PTY_RANGE, depsStatus, @@ -1199,8 +1199,7 @@ async function attachTerminal( && control.type === 'resize' && typeof control.cols === 'number' && typeof control.rows === 'number' ) { - const dims = clampDims(control.cols, control.rows) - handle.pty.resize(dims.cols, dims.rows) + tryResizePty(handle.pty, control.cols, control.rows) } else { handle.pty.write(text) } @@ -1268,8 +1267,7 @@ function pumpAgentTerminal( && control.type === 'resize' && typeof control.cols === 'number' && typeof control.rows === 'number' ) { - const dims = clampDims(control.cols, control.rows) - handle.pty.resize(dims.cols, dims.rows) + tryResizePty(handle.pty, control.cols, control.rows) } else if (control === null) { // Raw text input (a JSON-looking string the pty would have received // verbatim is reachable in theory but is exotic for an agent terminal; diff --git a/tests/agent-pty.spec.ts b/tests/agent-pty.spec.ts index f3530fd..88896c6 100644 --- a/tests/agent-pty.spec.ts +++ b/tests/agent-pty.spec.ts @@ -5,8 +5,14 @@ * console noise from node-pty's ConPTY module on Windows is expected and * does not affect the assertions. */ -import { describe, expect, it } from 'vitest' -import { AgentPtyRegistry, ALLOWED_SIGNALS, snapshotOf, type AgentTerminalSnapshot } from '../src/agent-pty.ts' +import { describe, expect, it, vi } from 'vitest' +import { + AgentPtyRegistry, + ALLOWED_SIGNALS, + snapshotOf, + tryResizePty, + type AgentTerminalSnapshot, +} from '../src/agent-pty.ts' /** * Resolve a shell binary for tests: on Windows use PowerShell (available on @@ -33,6 +39,35 @@ async function waitForTranscript( return handle?.transcript ?? '' } +describe('tryResizePty', () => { + it('clamps valid dimensions before resizing', () => { + const resize = vi.fn() + + expect(tryResizePty({ resize }, 5000, 80.9)).toBe(true) + expect(resize).toHaveBeenCalledWith(1024, 80) + }) + + it('contains a node-pty resize failure', () => { + const resize = vi.fn(() => { + throw new Error('Usage: pty.resize(fd, cols, rows, xPixel, yPixel)') + }) + + expect(tryResizePty({ resize }, 80, 24)).toBe(false) + expect(resize).toHaveBeenCalledWith(80, 24) + }) + + it.each([ + [Number.NaN, 24], + [Number.POSITIVE_INFINITY, 24], + [80, Number.NEGATIVE_INFINITY], + ])('rejects non-finite dimensions without calling node-pty (%s, %s)', (cols, rows) => { + const resize = vi.fn() + + expect(tryResizePty({ resize }, cols, rows)).toBe(false) + expect(resize).not.toHaveBeenCalled() + }) +}) + describe('AgentPtyRegistry', () => { it('creates a terminal with a uuid, writes the command to stdin, and lists it', async () => { const registry = new AgentPtyRegistry(testShell())