diff --git a/src/renderer/src/components/terminal-pane/pty-connection.test.ts b/src/renderer/src/components/terminal-pane/pty-connection.test.ts index cf834fab5..393996707 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.test.ts @@ -6390,7 +6390,7 @@ describe('connectPanePty', () => { expect(transport.sendInput).not.toHaveBeenCalled() }) - it('sends fast startup commands via sendInput for SSH connections', async () => { + it('waits for shell-ready before unhinted SSH startup commands', async () => { // Capture the setTimeout callback directly so we can fire it without vi.useFakeTimers() (which would also replace beforeEach's rAF mock). const pendingTimeouts: (() => void)[] = [] const originalSetTimeout = globalThis.setTimeout @@ -6414,7 +6414,7 @@ describe('connectPanePty', () => { ) transportFactoryQueue.push(transport) - // SSH connection: connectionId set; relay gets command metadata for spawn context but the renderer owns fast command delivery. + // SSH connection: the relay gets command metadata while the renderer owns delivery. mockStoreState = { ...mockStoreState, tabsByWorktree: { 'wt-1': [{ id: 'tab-1', ptyId: null }] }, @@ -6430,14 +6430,17 @@ describe('connectPanePty', () => { connectPanePty(pane as never, manager as never, deps as never) expect(capturedDataCallback.current).not.toBeNull() - // Simulate shell prompt arriving — queues the debounce timer capturedDataCallback.current?.('user@remote $ ') + expect(transport.sendInput).not.toHaveBeenCalled() - // Fire all queued setTimeout callbacks (the debounce) - for (const fn of pendingTimeouts) { + capturedDataCallback.current?.('\x1b]777;orca-shell-ready\x07user@remote $ ') + for (const fn of pendingTimeouts.splice(0)) { fn() } + expect(createdTransportOptions[0]).toEqual( + expect.objectContaining({ startupCommandDelivery: 'shell-ready' }) + ) expect(transport.sendInput).toHaveBeenCalledWith("claude 'say test'\r") } finally { globalThis.setTimeout = originalSetTimeout @@ -8234,7 +8237,10 @@ describe('connectPanePty', () => { connectPanePty(pane as never, manager as never, deps as never) await flushAsyncTicks(20) capturedDataCallback.current?.('user@remote $ ') - for (const fn of pendingTimeouts) { + expect(transport.sendInput).not.toHaveBeenCalled() + + capturedDataCallback.current?.('\x1b]777;orca-shell-ready\x07user@remote $ ') + for (const fn of pendingTimeouts.splice(0)) { fn() } @@ -8243,6 +8249,7 @@ describe('connectPanePty', () => { 2, expect.objectContaining({ command: "codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'codex-session-1'", + startupCommandDelivery: 'shell-ready', env: expect.objectContaining({ ORCA_PANE_KEY: paneKey, ORCA_TAB_ID: 'tab-1', diff --git a/src/renderer/src/components/terminal-pane/pty-connection.ts b/src/renderer/src/components/terminal-pane/pty-connection.ts index dea08c2af..908d703b1 100644 --- a/src/renderer/src/components/terminal-pane/pty-connection.ts +++ b/src/renderer/src/components/terminal-pane/pty-connection.ts @@ -128,8 +128,6 @@ import { } from '../../../../shared/terminal-mode-reset-profiles' import { buildFreshShellViewportBlankingSequence } from './terminal-restored-viewport' import { createShellReadyMarkerScanState, scanForShellReadyMarker } from './shell-ready-marker-scan' -import { shouldUseShellReadyStartupDelivery } from '../../../../shared/codex-startup-delivery' -import { resolveSetupAgentSequenceLaunchCommand } from '../../../../shared/setup-agent-sequencing' import { getSystemPrefersDark } from '@/lib/terminal-theme' import { INITIAL_MODE_2031_REPLY_SCAN_STATE, @@ -3810,7 +3808,9 @@ export function connectPanePty( command: shouldDeliverStartupViaTerminalPaste ? undefined : paneStartup?.command, startupCommandDelivery: shouldDeliverStartupViaTerminalPaste ? undefined - : paneStartup?.startupCommandDelivery, + : connectionId && paneStartup?.command + ? 'shell-ready' + : paneStartup?.startupCommandDelivery, connectionId, executionHostId, worktreeId: deps.worktreeId, @@ -4794,21 +4794,31 @@ export function connectPanePty( ? { command: paneStartup.command } : null : null - const startupShellReadyCommandHint = resolveSetupAgentSequenceLaunchCommand( - paneStartup?.env ?? {}, - paneStartup?.command - ) + // Why: every renderer-delivered SSH command needs the relay's readiness marker; + // the existing fallback preserves shells that cannot emit it. const shouldWaitForSshShellReady = Boolean(connectionId) && - shouldUseShellReadyStartupDelivery({ - command: startupShellReadyCommandHint, - startupCommandDelivery: paneStartup?.startupCommandDelivery - }) && + Boolean(pendingStartupCommand) && !shouldDeliverStartupViaTerminalPaste - const sshShellReadyMarkerScan = shouldWaitForSshShellReady + let sshShellReadyMarkerScan = shouldWaitForSshShellReady ? createShellReadyMarkerScanState() : null let sshStartupShellReady = !shouldWaitForSshShellReady + const armSshStartupShellReady = (): void => { + if (!connectionId || !pendingStartupCommand || shouldDeliverStartupViaTerminalPaste) { + return + } + if (startupInjectTimer !== null) { + clearTimeout(startupInjectTimer) + startupInjectTimer = null + } + if (sshShellReadyFallbackTimer !== null) { + clearTimeout(sshShellReadyFallbackTimer) + sshShellReadyFallbackTimer = null + } + sshShellReadyMarkerScan = createShellReadyMarkerScanState() + sshStartupShellReady = false + } const markSshStartupShellReady = (): void => { if (sshStartupShellReady) { return @@ -5121,7 +5131,8 @@ export function connectPanePty( return transport.sendInput('\r') } const schedulePendingStartupCommandDelivery = (): void => { - if (!pendingStartupCommand) { + const startup = pendingStartupCommand + if (!startup) { return } if (!sshStartupShellReady) { @@ -5142,8 +5153,7 @@ export function connectPanePty( startupInjectTimer = setTimeout(() => { startupInjectTimer = null void (async () => { - const startup = pendingStartupCommand - if (!startup || disposed) { + if (pendingStartupCommand !== startup || disposed) { return } if (shouldDeliverStartupViaTerminalPaste) { @@ -5242,6 +5252,7 @@ export function connectPanePty( // must still submit the resume command to the fresh remote shell. pendingStartupCommand = { command: startupOverride.command } } + armSshStartupShellReady() const coldRestoreOverride = startupOverride && 'launchConfig' in startupOverride ? (startupOverride as ColdRestoreAgentResumeStartup) @@ -5264,6 +5275,9 @@ export function connectPanePty( cols, rows, ...(startupOverride?.command ? { command: startupOverride.command } : {}), + ...(connectionId && startupOverride?.command + ? { startupCommandDelivery: 'shell-ready' as const } + : {}), ...(startupOverride?.env ? { env: mergeStartupEnvWithPaneIdentity(startupOverride.env) } : {}),