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
This commit is contained in:
Brennan Benson
2026-10-13 13:38:47 -07:00
committed by GitHub
parent 6cfe678dbe
commit 0903ed9577
2 changed files with 85 additions and 8 deletions
@@ -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<typeof connectPanePty>
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) => {
@@ -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()
}
}
}
}