fix(terminal): wait for shell-ready before every SSH startup command (#12963)
* fix(terminal): wait for shell-ready before every SSH startup command Renderer-delivered SSH launches wrote the startup command after a flat 50ms, so a remote shell still reaching its prompt could drop it. Only Codex plans opted into waiting. Gate on whether a startup command exists rather than on which agent it names: an unready shell drops whatever is written to it regardless. The relay already gates its own delivery on the marker it armed; this mirrors that for the renderer path, bounded by the existing 1.5s fallback. * fix(terminal): arm SSH marker for every startup * fix(terminal): cancel stale SSH startup writes
This commit is contained in:
parent
43bad61a93
commit
b09f7bd431
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
: {}),
|
||||
|
|
|
|||
Loading…
Reference in New Issue