diff --git a/src/main/ipc/pty/delivery/attached-pty-size.test.ts b/src/main/ipc/pty/delivery/attached-pty-size.test.ts index 75c3c45d99f..c117e3c30ef 100644 --- a/src/main/ipc/pty/delivery/attached-pty-size.test.ts +++ b/src/main/ipc/pty/delivery/attached-pty-size.test.ts @@ -98,6 +98,16 @@ describe('resolveCommittedPtySize', () => { ).toEqual(CACHED) }) + it('rejects a non-integer provider grid as unproven', () => { + expect( + resolveCommittedPtySize({ + result: { isReattach: true, snapshotCols: 120.5, snapshotRows: 40 }, + requested: REQUESTED, + cachedBeforeAttach: CACHED + }) + ).toEqual(CACHED) + }) + it('takes the request only when nothing better exists', () => { expect( resolveCommittedPtySize({ diff --git a/src/main/ipc/pty/delivery/attached-pty-size.ts b/src/main/ipc/pty/delivery/attached-pty-size.ts index c799dd74be3..d16f560a9e9 100644 --- a/src/main/ipc/pty/delivery/attached-pty-size.ts +++ b/src/main/ipc/pty/delivery/attached-pty-size.ts @@ -6,8 +6,8 @@ export type PtyGrid = { cols: number; rows: number } function positiveGrid(cols: unknown, rows: unknown): PtyGrid | undefined { return typeof cols === 'number' && typeof rows === 'number' && - Number.isFinite(cols) && - Number.isFinite(rows) && + Number.isInteger(cols) && + Number.isInteger(rows) && cols > 0 && rows > 0 ? { cols, rows } diff --git a/src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts b/src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts new file mode 100644 index 00000000000..128ccb51021 --- /dev/null +++ b/src/main/ipc/pty/runtime/spawn-commit-pty-size.test.ts @@ -0,0 +1,56 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { ptySizes } from '../delivery/visibility-state' +import { commitRuntimePtySpawn } from './spawn-commit' +import { createRuntimePtySpawnState, type RuntimePtySpawnArgs } from './spawn-state' +import type { PtyRuntimeControllerDeps } from './controller-deps' + +const PTY_ID = 'orca-pty-adopted' +const LIVE_GRID = { cols: 211, rows: 57 } + +function makeRuntime() { + return { + registerPreAllocatedHandleForPty: vi.fn(), + registerPty: vi.fn(), + reflowHeadlessTerminalToPtyGrid: vi.fn(), + seedHeadlessTerminal: vi.fn(), + noteTerminalSpawnCommand: vi.fn() + } +} + +describe('runtime spawn commit: adopted agent-session claim', () => { + afterEach(() => { + ptySizes.delete(PTY_ID) + }) + + it('commits the live grid from the adoption reply before the early return', async () => { + const runtime = makeRuntime() + const deps = { runtime, store: undefined, options: {} } as unknown as PtyRuntimeControllerDeps + const args = { cols: 120, rows: 40, worktreeId: 'wt-1' } as unknown as RuntimePtySpawnArgs + const ctx = createRuntimePtySpawnState(deps, args) + ctx.result = { + id: PTY_ID, + isReattach: true, + snapshotCols: LIVE_GRID.cols, + snapshotRows: LIVE_GRID.rows, + agentSessionEnsure: { + disposition: 'adopted', + owner: { + claim: { kind: 'terminal' }, + generation: 'g1', + phase: 'live', + ptyId: PTY_ID, + surface: { worktreeId: 'wt-1', tabId: 'tab-1', leafId: 'leaf-1', terminalHandle: 'h1' } + } + } + } as unknown as typeof ctx.result + + await commitRuntimePtySpawn(ctx) + + expect(ptySizes.get(PTY_ID)).toEqual(LIVE_GRID) + expect(runtime.reflowHeadlessTerminalToPtyGrid).toHaveBeenCalledWith( + PTY_ID, + LIVE_GRID.cols, + LIVE_GRID.rows + ) + }) +}) diff --git a/src/main/ipc/pty/runtime/spawn-commit-pty-size.ts b/src/main/ipc/pty/runtime/spawn-commit-pty-size.ts new file mode 100644 index 00000000000..a11bdccae15 --- /dev/null +++ b/src/main/ipc/pty/runtime/spawn-commit-pty-size.ts @@ -0,0 +1,18 @@ +import { commitAttachedPtySize } from '../delivery/attached-pty-size' +import type { RuntimePtySpawnState } from './spawn-state' + +/** Record the settled grid for a runtime-path spawn; `result` is passed explicitly because the + * adopted-claim branch commits with `isReattach` forced on before it returns early. */ +export function commitRuntimePtySize( + ctx: RuntimePtySpawnState, + result: RuntimePtySpawnState['result'] +): void { + commitAttachedPtySize({ + result, + requested: { cols: ctx.args.cols, rows: ctx.args.rows }, + cachedBeforeAttach: ctx.sessionSizeBeforeAttach, + reflowHeadlessTerminalToPtyGrid: ctx.deps.runtime?.reflowHeadlessTerminalToPtyGrid?.bind( + ctx.deps.runtime + ) + }) +} diff --git a/src/main/ipc/pty/runtime/spawn-commit.ts b/src/main/ipc/pty/runtime/spawn-commit.ts index 1fb95236664..94af728fe12 100644 --- a/src/main/ipc/pty/runtime/spawn-commit.ts +++ b/src/main/ipc/pty/runtime/spawn-commit.ts @@ -1,7 +1,7 @@ import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id' import { ptyOwnership, ptyIncarnationById, deletePtyOwnership } from '../provider/ownership-state' import { ptySizes } from '../delivery/visibility-state' -import { commitAttachedPtySize } from '../delivery/attached-pty-size' +import { commitRuntimePtySize } from './spawn-commit-pty-size' import { shouldSkipCodexHomeEnvForWindowsShell, recordCodexPaneAccountForSpawn, @@ -87,6 +87,9 @@ export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) { ...(ctx.env ? { launchEnv: ctx.env } : {}) }) } + // Why: an adoption attaches to a live session too, and this branch returns before the + // normal commit site; without this the cache keeps whatever the caller requested. + commitRuntimePtySize(ctx, { ...ctx.result, isReattach: true }) // Why: the adopted branch returns before the normal settle site, so the // reservation must be resolved here or every later spawn for this pane // awaits a promise that never settles. @@ -126,14 +129,7 @@ export async function commitRuntimePtySpawn(ctx: RuntimePtySpawnState) { if (!ctx.hostSessionBinding) { persistSshLease() } - commitAttachedPtySize({ - result: ctx.result, - requested: { cols: args.cols, rows: args.rows }, - cachedBeforeAttach: ctx.sessionSizeBeforeAttach, - reflowHeadlessTerminalToPtyGrid: ctx.deps.runtime?.reflowHeadlessTerminalToPtyGrid?.bind( - ctx.deps.runtime - ) - }) + commitRuntimePtySize(ctx, ctx.result) if (ctx.effectiveSessionAppId !== undefined && ctx.effectiveSessionAppId !== ctx.result.id) { ptySizes.delete(ctx.effectiveSessionAppId) }