From 0903ed9577fc46ce29e34ea49924dfa4fb5dc1ad Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:38:47 -0700 Subject: [PATCH] Start a startup terminal while Orca's window is hidden instead of waiting for it to be shown (#28351) * fix(terminal): don't hold a startup command for the grid settle while the window is hidden A startup pane waits for its grid to stop changing, counted in animation frames, so a TUI isn't drawn at a transient size. A covered or minimized window produces no frames, so the launch waited until Orca was visible again. Skip the settle while the window is hidden, as hidden panes already do, and end an in-flight settle when the window becomes hidden. * test(terminal): pass connectPanePty doubles through one checked cast --- .../pty-connection-setup-split-spawn.test.ts | 57 +++++++++++++++++++ .../pty-connection/pty-resize-geometry.ts | 36 +++++++++--- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts b/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts index 7c64b7d1468..5bb1412c42a 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection-setup-split-spawn.test.ts @@ -341,6 +341,63 @@ describe('connectPanePty', () => { expect(transport.connect).toHaveBeenCalledWith(expect.objectContaining({ cols: 120, rows: 50 })) }) + it.each(['before', 'during'] as const)( + 'spawns a startup command without waiting for frames when the window is hidden %s the grid settle', + async (hiddenWhen) => { + // A hidden window produces no animation frames, so only the queued ones below ever run. + const frameCallbacks: FrameRequestCallback[] = [] + globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => { + frameCallbacks.push(callback) + return frameCallbacks.length + }) + const visibilityListeners: (() => void)[] = [] + const fakeDocument = { + visibilityState: hiddenWhen === 'before' ? 'hidden' : 'visible', + addEventListener: vi.fn((type: string, listener: () => void) => { + if (type === 'visibilitychange') { + visibilityListeners.push(listener) + } + }), + removeEventListener: vi.fn() + } + vi.stubGlobal('document', fakeDocument) + vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + + const { connectPanePty } = await import('./pty-connection') + const transport = createMockTransport() + transportFactoryQueue.push(transport) + const pane = createPane(1) + pane.container = createMeasuredElement({ rect: () => createRect(1200, 800) }) + pane.fitAddon.proposeDimensions = vi.fn(() => ({ cols: 120, rows: 40 })) + pane.fitAddon.fit = vi.fn(() => { + pane.terminal.cols = 120 + pane.terminal.rows = 40 + }) + const deps = createDeps({ startup: { command: 'claude' } }) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pane, manager and deps doubles implement every member connectPanePty reads on this startup path. + const args = [pane, createManager(1), deps] as unknown as Parameters + connectPanePty(...args) + + if (hiddenWhen === 'before') { + vi.advanceTimersByTime(250) + } else { + for (let i = 0; i < 3; i++) { + frameCallbacks.shift()?.(0) + } + expect(transport.connect).not.toHaveBeenCalled() + fakeDocument.visibilityState = 'hidden' + for (const listener of visibilityListeners) { + listener() + } + } + + expect(createdTransportOptions[0]?.command).toBe('claude') + expect(transport.connect).toHaveBeenCalledWith( + expect.objectContaining({ cols: 120, rows: 40 }) + ) + } + ) + it('spawns the delayed main setup-split pane when its sibling owns the tab PTY fallback', async () => { const frameCallbacks: FrameRequestCallback[] = [] globalThis.requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => { diff --git a/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts b/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts index 441141069c5..095668d4de6 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection/pty-resize-geometry.ts @@ -8,6 +8,7 @@ import { reconcilePtySizeAcrossFrames } from '../pty-size-reconcile' import { shouldClaimRemoteDesktopViewport } from '../remote-desktop-viewport-claim' import { deferTerminalGeometryMutationDuringRebuild } from '@/lib/pane-manager/terminal-scroll-intent-rebuild' import { waitForStableStartupGrid } from '../terminal-startup-grid-settle' +import { getWindowParkVisible, subscribeWindowParkVisibility } from '@/lib/window-park-visibility' import { isSetupSplitGeometryReady } from './setup-split-geometry' @@ -207,9 +208,12 @@ export function installPtyResizeGeometry(session: ConnectPanePtySession): void { const rows = session.pane.terminal.rows return cols > 0 && rows > 0 ? { cols, rows } : null } + // Why: the settle only prevents visible artifacts, and a hidden pane or window paints none + // (nor produces the frames the settle counts). session.shouldSettleStartupGridBeforeConnect = (): boolean => Boolean(session.paneStartup?.command) && session.deps.isVisibleRef.current && + getWindowParkVisible() && !session.unservedSshTargetId && session.runtimeEnvironmentId === null session.isStartupGridReadyForConnect = (): boolean => { @@ -223,7 +227,14 @@ export function installPtyResizeGeometry(session: ConnectPanePtySession): void { } session.settleStartupGridBeforeConnect = (connect: () => void): void => { session.startupGridSettleHandle?.cancel() - let settledSynchronously = false + let settled = false + let unsubscribeWindowVisibility = (): void => {} + const finish = (): void => { + settled = true + unsubscribeWindowVisibility() + session.startupGridSettleHandle = null + connect() + } // Why: local startup commands can launch a TUI before the split-pane grid // has settled; spawn from a briefly stable grid so the TUI paints cleanly. const handle = waitForStableStartupGrid({ @@ -232,11 +243,7 @@ export function installPtyResizeGeometry(session: ConnectPanePtySession): void { ? session.isStartupGridReadyForConnect : undefined, measure: session.measureStartupGrid, - onSettled: () => { - settledSynchronously = true - session.startupGridSettleHandle = null - connect() - }, + onSettled: finish, requestFrame: (callback) => requestAnimationFrame(callback), cancelFrame: (handle) => { if (typeof cancelAnimationFrame === 'function') { @@ -244,8 +251,21 @@ export function installPtyResizeGeometry(session: ConnectPanePtySession): void { } } }) - if (!settledSynchronously) { - session.startupGridSettleHandle = handle + if (settled) { + return + } + // Why: a window hidden mid-settle stops producing frames; spawn now, not when it is shown again. + unsubscribeWindowVisibility = subscribeWindowParkVisibility(() => { + if (!getWindowParkVisible()) { + handle.cancel() + finish() + } + }) + session.startupGridSettleHandle = { + cancel: () => { + handle.cancel() + unsubscribeWindowVisibility() + } } } }