fix(terminal): a new SSH terminal opened before sign-in waits and starts after sign-in (#33072)

* fix(terminal): a new SSH terminal opened before sign-in waits and starts after sign-in

A terminal created while its managed SSH host is still waiting for a typed
password or passphrase got main's sign-in refusal from terminal.create and
showed it as a fatal error. Signing in later never started it.

Route that refusal into the same parked retry #32275 gave restored panes, so
the post-sign-in control-ready revival replays the create. The pane shows a
'Waiting for SSH sign-in' banner with a Sign in action instead of an error.

* fix(terminal): clear the sign-in wait as soon as sign-in finishes

Publish the sign-in reason through one setter so clearing it always reaches
the banner, and clear it when the control socket proves sign-in finished.
The Sign in button retries only after a successful connect, so a rejected
password cannot re-dial into a second prompt. Share one banner layout.

* fix(terminal): keep a sign-in-waiting spawn pending until the replayed create lands

The pane's spawn finalization (launch config registration, startup delivery)
ran against an undefined result while the create waited for SSH sign-in,
then never saw the terminal the post-sign-in replay made. Keep the original
connect pending and settle it with the replay's result, or with nothing when
the pane is closed, re-attached or a fresh connect supersedes it.

* fix(terminal): a stale replay settles only its own sign-in wait

* fix(terminal): settle a sign-in-waiting spawn at the recovery state, not per replay

Codex and Opus found recoveries that finish outside a create replay (the
stream resubscribe after the create landed, an ownership refresh) and left
the original connect pending. Settle it at the two points every recovery
passes: healthy (with the bound terminal) and idle or disposed (with nothing).

* fix(i18n): clearer Spanish sign-in wait copy
This commit is contained in:
Neil
2026-10-24 00:35:20 -07:00
committed by GitHub
parent 55ed5f8757
commit 06211d1406
13 changed files with 429 additions and 58 deletions
@@ -189,6 +189,7 @@ export function TerminalPaneRecoveryPortals({
if (!recoveryState || recoveryState.reason === 'ownership') {
return null
}
const awaitingSignIn = recoveryState.reason === 'ssh-sign-in'
return createPortal(
<DelayedRemoteRuntimeReconnectBanner
// Why per pane, not per epoch: a Reconnect click opens a new epoch and must not hide the banner again.
@@ -207,10 +208,27 @@ export function TerminalPaneRecoveryPortals({
.finally(retry)
return
}
const signInEnvironmentId = awaitingSignIn
? paneTransportsRef.current.get(pane.id)?.getRuntimeEnvironmentId?.()
: null
if (signInEnvironmentId) {
// Why no retry on failure: a rejected password must not re-dial into a second prompt;
// the control-ready revival starts the pane once sign-in succeeds.
void window.api.runtimeEnvironments
.connect({ selector: signInEnvironmentId })
.then((response) => {
if (response.ok) {
retry()
}
})
.catch(() => undefined)
return
}
retry()
}}
wslHostUnavailable={wslHostUnavailable}
pairingRefused={pairingRefusedEnvironmentId !== null}
awaitingSignIn={awaitingSignIn}
/>,
pane.container,
`remote-runtime-reconnect-${pane.id}`
@@ -57,6 +57,29 @@ describe('TerminalRemoteRuntimeReconnectBanner', () => {
})
})
describe('SSH sign-in wait', () => {
it.each(['backoff', 'disconnected'] as const)(
'shows the sign-in wait at once during %s, never the runtime-lost copy',
async (phase) => {
const onReconnect = vi.fn()
const user = userEvent.setup()
render(
<DelayedRemoteRuntimeReconnectBanner
epoch={1}
phase={phase}
onReconnect={onReconnect}
awaitingSignIn
/>
)
expect(screen.getByText('Waiting for SSH sign-in')).toBeInTheDocument()
expect(screen.queryByText(/Remote runtime disconnected|Reconnecting/)).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'Sign in' }))
expect(onReconnect).toHaveBeenCalledOnce()
}
)
})
describe('DelayedRemoteRuntimeReconnectBanner', () => {
it('keeps a brief resubscribe from flashing the retrying banner', () => {
vi.useFakeTimers()
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Loader2, ServerOff } from 'lucide-react'
import { KeyRound, Loader2, ServerOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { translate } from '@/i18n/i18n'
import type { PtyTransportRecoveryState } from './pty-transport-types'
@@ -38,7 +38,8 @@ export function TerminalRemoteRuntimeReconnectBanner({
phase,
onReconnect,
wslHostUnavailable = null,
pairingRefused = false
pairingRefused = false,
awaitingSignIn = false
}: {
phase: VisibleRecoveryPhase
onReconnect: () => void
@@ -46,16 +47,77 @@ export function TerminalRemoteRuntimeReconnectBanner({
wslHostUnavailable?: WslHostUnavailableReason | null
/** The pane's managed server refuses this desktop and pairing again over SSH failed. */
pairingRefused?: boolean
/** Main refused because the SSH host is waiting for the user to sign in; it resumes after. */
awaitingSignIn?: boolean
}): React.JSX.Element {
if (wslHostUnavailable) {
return <WslHostUnavailableBanner reason={wslHostUnavailable} />
}
const retrying = phase !== 'disconnected' && !pairingRefused
const signingIn = awaitingSignIn && !pairingRefused
const retrying = phase !== 'disconnected' && !pairingRefused && !signingIn
const reconnectLabel = translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.reconnectButton',
'Reconnect'
)
const copy = pairingRefused
? {
bannerState: 'pairing-refused',
title: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.pairingRefusedTitle',
'This host no longer accepts this computer'
),
body: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.pairingRefusedBody',
"Orca couldn't pair with it again over SSH. Nothing on the host was stopped. Reconnect to try again."
),
action: reconnectLabel
}
: signingIn
? {
bannerState: 'ssh-sign-in',
title: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.signInTitle',
'Waiting for SSH sign-in'
),
body: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.signInBody',
'This terminal continues by itself once you sign in to the host.'
),
action: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.signInButton',
'Sign in'
)
}
: retrying
? {
bannerState: phase,
title: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.retryingTitle',
'Reconnecting to remote runtime'
),
body: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.retryingBody',
'Orca is retrying automatically. This terminal will resume if the connection returns.'
),
action: null
}
: {
bannerState: phase,
title: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.disconnectedTitle',
'Remote runtime disconnected'
),
body: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.disconnectedBody',
'Automatic retries stopped. Reconnect to resume this terminal session.'
),
action: reconnectLabel
}
return (
<div
className="pointer-events-none absolute inset-x-3 bottom-3 z-30 flex justify-center"
data-terminal-remote-runtime-reconnect-banner={pairingRefused ? 'pairing-refused' : phase}
data-terminal-remote-runtime-reconnect-banner={copy.bannerState}
>
<div
className="pointer-events-auto flex w-full max-w-xl items-center gap-3 rounded-md border border-border bg-card/95 px-3 py-3 text-card-foreground shadow-xs backdrop-blur-[1px]"
@@ -65,50 +127,19 @@ export function TerminalRemoteRuntimeReconnectBanner({
<div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-muted text-muted-foreground">
{retrying ? (
<Loader2 className="size-4 animate-spin" />
) : signingIn ? (
<KeyRound className="size-4" />
) : (
<ServerOff className="size-4" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-semibold">
{pairingRefused
? translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.pairingRefusedTitle',
'This host no longer accepts this computer'
)
: retrying
? translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.retryingTitle',
'Reconnecting to remote runtime'
)
: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.disconnectedTitle',
'Remote runtime disconnected'
)}
</div>
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
{pairingRefused
? translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.pairingRefusedBody',
"Orca couldn't pair with it again over SSH. Nothing on the host was stopped. Reconnect to try again."
)
: retrying
? translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.retryingBody',
'Orca is retrying automatically. This terminal will resume if the connection returns.'
)
: translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.disconnectedBody',
'Automatic retries stopped. Reconnect to resume this terminal session.'
)}
</div>
<div className="text-sm font-semibold">{copy.title}</div>
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">{copy.body}</div>
</div>
{!retrying ? (
{copy.action ? (
<Button size="sm" onClick={onReconnect}>
{translate(
'auto.components.terminal.pane.TerminalRemoteRuntimeReconnectBanner.reconnectButton',
'Reconnect'
)}
{copy.action}
</Button>
) : null}
</div>
@@ -125,6 +156,7 @@ export function DelayedRemoteRuntimeReconnectBanner({
}): React.JSX.Element | null {
const revealed = useRemoteRuntimeRecoveryBannerRevealed(
props.phase === 'disconnected' ||
Boolean(props.awaitingSignIn) ||
Boolean(props.wslHostUnavailable) ||
Boolean(props.pairingRefused)
)
@@ -154,7 +154,7 @@ type PtyCallbacks = {
}
export type PtyTransportRecoveryState = {
reason?: 'ownership'
reason?: 'ownership' | 'ssh-sign-in'
phase:
| 'connecting'
| 'connected'
@@ -342,6 +342,22 @@ export function createRemoteRuntimePtyTransport(
const disconnectedInputGrace = createRemoteRuntimeDisconnectedInputGrace()
let previousRecoveryPhase: RemoteRuntimePtyRecoveryPhase = 'idle'
let unregisterControlReadyReviver: (() => void) | null = null
// Why: the pane's retry is waiting on the user's SSH sign-in, not on a lost link.
let awaitingSshSignIn = false
// Why: the caller's spawn finalization (launch config, startup delivery) must see the terminal
// recovery binds after sign-in, so its connect stays pending until recovery settles either way.
let signInConnectWait: PromiseWithResolvers<PtyConnectResult | undefined> | null = null
function settleSignInConnectWait(result?: PtyConnectResult): void {
const wait = signInConnectWait
signInConnectWait = null
wait?.resolve(result)
}
function setAwaitingSshSignIn(next: boolean): void {
if (awaitingSshSignIn !== next) {
awaitingSshSignIn = next
emitRecoveryState()
}
}
// Why registered only while recovering: detach and destroy go idle, so a discarded transport is never retained.
function syncControlReadyReviver(): void {
const wantsRevival = recovery.isActive || recovery.currentPhase === 'disconnected'
@@ -349,7 +365,14 @@ export function createRemoteRuntimePtyTransport(
// Why: a backoff advances and a latched pane replays its attach, exactly as online/resume/Reconnect would.
unregisterControlReadyReviver = registerRemoteRuntimePtyControlReadyReviver(
runtimeEnvironmentId,
() => !destroyed && (recovery.retryNow() || (transport.retryRecovery?.() ?? false))
() => {
if (destroyed) {
return false
}
// Why: a ready control socket proves sign-in finished, whatever the replay meets next.
setAwaitingSshSignIn(false)
return recovery.retryNow() || (transport.retryRecovery?.() ?? false)
}
)
} else if (!wantsRevival && unregisterControlReadyReviver) {
unregisterControlReadyReviver()
@@ -384,6 +407,11 @@ export function createRemoteRuntimePtyTransport(
subscriptionGeneration += 1
closeMultiplexedStream()
}
if (recovery.currentPhase === 'idle' || recovery.currentPhase === 'disposed') {
awaitingSshSignIn = false
// Why: a healthy recovery settled it first with the bound terminal; anything else gave up.
settleSignInConnectWait()
}
if (recovery.currentPhase === 'idle') {
autoRecoveryWindowSpent = false
// Why before the reset: input that outlived the grace must not release into the recovered pane.
@@ -615,13 +643,20 @@ export function createRemoteRuntimePtyTransport(
: connected && attachmentReady
? 'connected'
: 'offline'
const ownership =
(recovery.isActive && recovery.hasOwnerRefresh) || surfacedErrorMessages.has(ownerChanged)
const awaitingSignIn =
awaitingSshSignIn &&
(phase === 'recovering' || phase === 'backoff' || phase === 'disconnected')
return {
phase,
epoch: recovery.currentEpoch,
attempt: recovery.attemptCount,
...((recovery.isActive && recovery.hasOwnerRefresh) || surfacedErrorMessages.has(ownerChanged)
...(ownership
? { reason: 'ownership' as const }
: {})
: awaitingSignIn
? { reason: 'ssh-sign-in' as const }
: {})
}
}
@@ -651,6 +686,20 @@ export function createRemoteRuntimePtyTransport(
}
function markRecoveryHealthy(result?: PtyConnectResult): void {
if (signInConnectWait) {
settleSignInConnectWait(
remotePtyId
? {
id: remotePtyId,
replay: '',
...(spawnedHandle === handle ? {} : { isReattach: true }),
...(authoritativePtyIncarnationId
? { incarnationId: authoritativePtyIncarnationId }
: {})
}
: undefined
)
}
const recoveredErrors = [...surfacedErrorMessages]
surfacedErrorMessages.clear()
recovery.markHealthy(result)
@@ -1043,7 +1092,7 @@ export function createRemoteRuntimePtyTransport(
return true
}
if (lastConnectOptions) {
void connectHoldingRestoredInput(lastConnectOptions)
void connectHoldingRestoredInput(lastConnectOptions, true)
return true
}
return false
@@ -1329,6 +1378,7 @@ export function createRemoteRuntimePtyTransport(
if (!isRecoverableRemoteRuntimeConnectionError(statusClientError)) {
throw statusError
}
setAwaitingSshSignIn(false)
const startsRecovery = recoveryDeadlineAt === null
recoveryDeadlineAt ??= Date.now() + REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS
if (startsRecovery && !recovery.isActive) {
@@ -1373,6 +1423,7 @@ export function createRemoteRuntimePtyTransport(
if (!isRecoverableRemoteRuntimeConnectionError(clientError)) {
throw error
}
setAwaitingSshSignIn(false)
if (kind === 'agent-session') {
agentSessionRequiresHostAuthorityReplay = true
} else {
@@ -3236,6 +3287,7 @@ export function createRemoteRuntimePtyTransport(
// Why: no handle is bound yet, so the resubscribe path cannot own this retry; replay attach
// instead of latching in 'connecting' with nothing armed (#18495).
connecting = false
setAwaitingSshSignIn(isSshSignInPendingTerminalError(runtimeTerminalErrorMessage(error)))
scheduleConnectRetryAfterRecoverableFailure()
if (handle && isRemoteTerminalStaleMessage(runtimeTerminalErrorMessage(error))) {
retireIfHostRecordsExit(handle, remotePtyId ?? options.existingPtyId ?? null)
@@ -3295,17 +3347,34 @@ export function createRemoteRuntimePtyTransport(
// Why: typing on a restored screen reaches its original terminal only, never a fresh create.
async function connectHoldingRestoredInput(
options: Parameters<PtyTransport['connect']>[0]
options: Parameters<PtyTransport['connect']>[0],
replay = false
): Promise<PtyConnectResult | undefined> {
if (!replay) {
settleSignInConnectWait()
}
const expected = recoveryInputHold.expect(restoredInputEndpoint(options.sessionId))
let result: PtyConnectResult | undefined
try {
return await connectPane(options)
result = await connectPane(options)
} finally {
recoveryInputHold.settleExpectation(expected, handle !== null && !destroyed)
if (handle && recoveryInputHold.isHolding()) {
releaseHeldInput()
}
}
const retryOwned = !destroyed && (recovery.isActive || recovery.currentPhase === 'disconnected')
if (
!replay &&
result === undefined &&
retryOwned &&
awaitingSshSignIn &&
lastConnectOptions === options
) {
signInConnectWait ??= Promise.withResolvers()
return signInConnectWait.promise
}
return result
}
async function connectPane(
@@ -3568,7 +3637,13 @@ export function createRemoteRuntimePtyTransport(
} else if (isRemoteTerminalGoneMessage(message)) {
recovery.cancel()
handleRemoteTerminalError(error)
} else if (isSshSignInPendingTerminalError(message)) {
// Why: main refused before dispatch, so nothing was created; the control-ready revival
// replays this create once sign-in finishes (#31942).
setAwaitingSshSignIn(true)
scheduleConnectRetryAfterRecoverableFailure()
} else if (isRecoverablePaneBindingError(error)) {
setAwaitingSshSignIn(false)
scheduleConnectRetryAfterRecoverableFailure()
} else {
recovery.cancel()
@@ -3587,11 +3662,13 @@ export function createRemoteRuntimePtyTransport(
connect: connectHoldingRestoredInput,
attach(options) {
settleSignInConnectWait()
recoveryInputHold.expect(null)
attachPane(options, false)
},
disconnect() {
settleSignInConnectWait()
lifecycleEpoch += 1
attachGeneration += 1
discardPendingInput()
@@ -3630,6 +3707,7 @@ export function createRemoteRuntimePtyTransport(
// Why first: the successor transport owns the PTY after detach, and the batcher flushes
// below can throw past the census drop — a stranded gauge outlives the transport.
outputProcessor.disposePendingSideEffectGauge()
settleSignInConnectWait()
lifecycleEpoch += 1
attachGeneration += 1
discardPendingInput()
@@ -3793,7 +3871,7 @@ export function createRemoteRuntimePtyTransport(
recovery.currentPhase === 'disconnected'
) {
recovery.begin()
void connectHoldingRestoredInput(lastConnectOptions)
void connectHoldingRestoredInput(lastConnectOptions, true)
return true
}
// Why: online/resume fires a parked retry; the button must not be weaker than an event (#12684).
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PtyTransport } from './pty-transport-types'
import type { PtyTransport, PtyTransportRecoveryState } from './pty-transport-types'
import {
createRemoteRuntimeTransportMocks,
readyHostSessionInventoryResponse,
@@ -21,6 +21,7 @@ let transport: PtyTransport | undefined
const {
runtimeCall,
runtimeSubscribe,
latestSubscribePayload,
subscribedTerminalHandles,
emitSnapshot,
@@ -120,7 +121,7 @@ describe.each([
)
)('restores original-terminal I/O for $name $timing', async (failure) => {
const { pane, onData, onError, signIn } = await mountAwaitingPane(tabId, failure)
expect(pane.getRecoveryState?.().phase).toBe('backoff')
expect(pane.getRecoveryState?.()).toMatchObject({ phase: 'backoff', reason: 'ssh-sign-in' })
expect(subscribedTerminalHandles()).toEqual([])
if (failure.late) {
@@ -183,3 +184,201 @@ describe.each([
expect(runtimeCall).toHaveBeenCalledTimes(callsAfterTeardown)
})
})
async function mountNewPaneAwaitingSignIn(failure: { message: string }, afterSignIn?: () => void) {
let signedIn = false
const healthyCall = runtimeCall.getMockImplementation()
runtimeCall.mockImplementation(async (request: { method: string; params?: unknown }) => {
if (!signedIn) {
throw new Error(failure.message)
}
afterSignIn?.()
return healthyCall?.(request)
})
const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport')
const { reviveRemoteRuntimePtyRecoveriesForEnvironment } =
await import('./remote-runtime-pty-recovery-state')
const onError = vi.fn()
const onPtySpawn = vi.fn()
const publishedStates: PtyTransportRecoveryState[] = []
const pane = createRemoteRuntimePtyTransport('env-1', {
worktreeId: 'wt-1',
tabId: 'tab-new',
leafId: 'pane:1',
onPtySpawn
})
transport = pane
let connectSettled = false
const connected = Promise.resolve(
pane.connect({
url: '',
cols: 80,
rows: 24,
callbacks: { onError, onRecoveryStateChange: (state) => publishedStates.push(state) }
})
).finally(() => {
connectSettled = true
})
await vi.advanceTimersByTimeAsync(0)
return {
connected,
connectSettled: () => connectSettled,
pane,
lastPublishedState: () => publishedStates.at(-1),
onError,
onPtySpawn,
createCalls: () =>
runtimeCall.mock.calls.filter(([request]) => request.method === 'terminal.create'),
signIn: () => {
signedIn = true
return reviveRemoteRuntimePtyRecoveriesForEnvironment('env-1')
}
}
}
describe('SSH sign-in for a terminal created while the host waits for sign-in', () => {
it.each(
pendingErrors.flatMap((failure) =>
[false, true].map((late) => ({
...failure,
late,
timing: late ? 'after the recovery deadline' : 'before the recovery deadline'
}))
)
)('waits for sign-in and then starts for $name $timing', async (failure) => {
const { pane, onError, onPtySpawn, signIn, connected, connectSettled } =
await mountNewPaneAwaitingSignIn(failure)
expect(pane.getRecoveryState?.()).toMatchObject({
phase: 'backoff',
reason: 'ssh-sign-in'
})
expect(onError).not.toHaveBeenCalled()
expect(connectSettled()).toBe(false)
if (failure.late) {
await vi.advanceTimersByTimeAsync(REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS + 1)
expect(pane.getRecoveryState?.()).toMatchObject({
phase: 'disconnected',
reason: 'ssh-sign-in'
})
}
expect(signIn()).toBe(1)
await vi.waitFor(() => expect(latestSubscribePayload().terminal).toBe('terminal-1'))
const { streamId } = latestSubscribePayload()
subscriptionCallbacks?.onResponse({
ok: true,
result: { type: 'subscribed', streamId }
})
emitSnapshot(streamId, 'fresh prompt$ ')
expect(onPtySpawn).toHaveBeenCalledTimes(1)
expect(onPtySpawn).toHaveBeenCalledWith('remote:env-1@@terminal-1')
// The original spawn settles with the post-sign-in create, so its finalization still runs.
await expect(connected).resolves.toMatchObject({ id: 'remote:env-1@@terminal-1' })
expect(pane.isConnected()).toBe(true)
expect(pane.getRecoveryState?.().reason).toBeUndefined()
expect(onError).not.toHaveBeenCalled()
})
it('starts after a sign-in that was cancelled and completed later', async () => {
const failure = { message: pendingErrors[0].message }
const pending = await mountNewPaneAwaitingSignIn(failure)
failure.message = pendingErrors[2].message
await vi.advanceTimersByTimeAsync(REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS + 1)
expect(pending.pane.getRecoveryState?.()).toMatchObject({
phase: 'disconnected',
reason: 'ssh-sign-in'
})
expect(pending.signIn()).toBe(1)
await vi.waitFor(() => expect(pending.onPtySpawn).toHaveBeenCalledTimes(1))
expect(pending.onError).not.toHaveBeenCalled()
})
it('settles the original spawn when the stream, not the create, waited for sign-in', async () => {
resolvedPaneHandle = 'terminal-1'
let signedIn = false
const healthySubscribe = runtimeSubscribe.getMockImplementation()
runtimeSubscribe.mockImplementation(async (...args: unknown[]) => {
if (!signedIn) {
throw new Error(pendingErrors[0].message)
}
return healthySubscribe?.(...args)
})
const { createRemoteRuntimePtyTransport } = await import('./remote-runtime-pty-transport')
const { reviveRemoteRuntimePtyRecoveriesForEnvironment } =
await import('./remote-runtime-pty-recovery-state')
const onPtySpawn = vi.fn()
const pane = createRemoteRuntimePtyTransport('env-1', {
worktreeId: 'wt-1',
tabId: 'tab-new',
leafId: 'pane:1',
onPtySpawn
})
transport = pane
const connected = Promise.resolve(pane.connect({ url: '', cols: 80, rows: 24, callbacks: {} }))
await vi.advanceTimersByTimeAsync(0)
expect(pane.getRecoveryState?.().reason).toBe('ssh-sign-in')
signedIn = true
expect(reviveRemoteRuntimePtyRecoveriesForEnvironment('env-1')).toBe(1)
await vi.waitFor(() => expect(latestSubscribePayload().terminal).toBe('terminal-1'))
const { streamId } = latestSubscribePayload()
subscriptionCallbacks?.onResponse({ ok: true, result: { type: 'subscribed', streamId } })
emitSnapshot(streamId, 'fresh prompt$ ')
await expect(connected).resolves.toMatchObject({ id: 'remote:env-1@@terminal-1' })
expect(onPtySpawn).toHaveBeenCalledTimes(1)
})
it('stops showing the sign-in wait once sign-in finishes, even while the create still retries', async () => {
const { pane, lastPublishedState, signIn } = await mountNewPaneAwaitingSignIn(
pendingErrors[0],
() => {
throw Object.assign(new Error('Could not connect to the remote Orca runtime.'), {
code: 'remote_runtime_unavailable'
})
}
)
expect(lastPublishedState()?.reason).toBe('ssh-sign-in')
signIn()
await vi.advanceTimersByTimeAsync(0)
expect(pane.getRecoveryState?.().phase).not.toBe('idle')
expect(lastPublishedState()?.reason).toBeUndefined()
})
it.each(['destroy', 'disconnect'] as const)(
'spawns nothing after sign-in once the pending terminal is closed (%s)',
async (teardown) => {
const { pane, onPtySpawn, signIn, createCalls, connected } = await mountNewPaneAwaitingSignIn(
pendingErrors[0]
)
const createsBeforeClose = createCalls().length
pane[teardown]?.()
await expect(connected).resolves.toBeUndefined()
expect(signIn()).toBe(0)
await vi.advanceTimersByTimeAsync(REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS + 1)
expect(createCalls()).toHaveLength(createsBeforeClose)
expect(onPtySpawn).not.toHaveBeenCalled()
expect(subscribedTerminalHandles()).toEqual([])
}
)
it('still reports rejected credentials instead of waiting', async () => {
const { pane, onError, signIn, createCalls } = await mountNewPaneAwaitingSignIn({
message: 'All configured authentication methods failed'
})
expect(onError).toHaveBeenCalledWith('All configured authentication methods failed')
expect(pane.getRecoveryState?.().reason).toBeUndefined()
const createsAfterFailure = createCalls().length
expect(signIn()).toBe(0)
await vi.advanceTimersByTimeAsync(REMOTE_RUNTIME_AUTO_RECOVERY_TIMEOUT_MS + 1)
expect(createCalls()).toHaveLength(createsAfterFailure)
})
})
+4 -1
View File
@@ -3829,7 +3829,10 @@
"disconnectedTitle": "Remote runtime disconnected",
"retryingBody": "Orca is retrying automatically. This terminal will resume if the connection returns.",
"disconnectedBody": "Automatic retries stopped. Reconnect to resume this terminal session.",
"reconnectButton": "Reconnect"
"reconnectButton": "Reconnect",
"signInTitle": "Waiting for SSH sign-in",
"signInBody": "This terminal continues by itself once you sign in to the host.",
"signInButton": "Sign in"
},
"TerminalQuickCommandsSubmenu": {
"3ccc7981bb": "Host unavailable",
+4 -1
View File
@@ -3526,7 +3526,10 @@
"disconnectedTitle": "Remote runtime disconnected",
"retryingBody": "Orca está reintentando automáticamente. Este terminal se reanudará si vuelve la conexión.",
"disconnectedBody": "Se detuvieron los reintentos automáticos. Vuelva a conectarse para reanudar esta sesión de terminal.",
"reconnectButton": "Reconnect"
"reconnectButton": "Reconnect",
"signInTitle": "Esperando el inicio de sesión SSH",
"signInBody": "Este terminal continuará por sí solo cuando inicie sesión en el host.",
"signInButton": "Iniciar sesión"
},
"TerminalRestoringPlaceholder": {
"restoring": "Restaurando terminal…"
+4 -1
View File
@@ -3695,7 +3695,10 @@
"disconnectedTitle": "Runtime distant déconnecté",
"retryingBody": "Orca réessaie automatiquement. Ce terminal reprendra si la connexion revient.",
"disconnectedBody": "Les tentatives automatiques sont interrompues. Reconnectez-vous pour reprendre cette session de terminal.",
"reconnectButton": "Reconnecter"
"reconnectButton": "Reconnecter",
"signInTitle": "En attente de la connexion SSH",
"signInBody": "Ce terminal reprend tout seul dès que vous vous connectez à l’hôte.",
"signInButton": "Se connecter"
},
"TerminalQuickCommandsSubmenu": {
"3ccc7981bb": "Hôte indisponible",
+4 -1
View File
@@ -3511,7 +3511,10 @@
"disconnectedTitle": "リモートランタイムが切断されました",
"retryingBody": "Orca は自動的に再試行します。接続が復元されると、このターミナルが再開されます。",
"disconnectedBody": "自動再試行が停止しました。このターミナルセッションを再開するには再接続してください。",
"reconnectButton": "再接続"
"reconnectButton": "再接続",
"signInTitle": "SSH のサインインを待っています",
"signInBody": "ホストにサインインすると、このターミナルは自動的に続行します。",
"signInButton": "サインイン"
},
"TerminalLinkActionPopover": {
"openLink": "リンクを開く",
+4 -1
View File
@@ -3511,7 +3511,10 @@
"disconnectedTitle": "원격 런타임 연결 끊김",
"retryingBody": "Orca가 자동으로 재시도합니다. 연결이 복원되면 이 터미널이 재개됩니다.",
"disconnectedBody": "자동 재시도가 중지되었습니다. 이 터미널 세션을 재개하려면 다시 연결하세요.",
"reconnectButton": "다시 연결"
"reconnectButton": "다시 연결",
"signInTitle": "SSH 로그인 대기 중",
"signInBody": "호스트에 로그인하면 이 터미널이 자동으로 계속됩니다.",
"signInButton": "로그인"
},
"TerminalLinkActionPopover": {
"openLink": "링크 열기",
+4 -1
View File
@@ -15652,7 +15652,10 @@
"disconnectedBody": "As tentativas automáticas foram interrompidas. Reconecte para retomar esta sessão de terminal.",
"reconnectButton": "Reconectar",
"pairingRefusedTitle": "Este host não aceita mais este computador",
"pairingRefusedBody": "O Orca não conseguiu parear com ele novamente via SSH. Nada no host foi interrompido. Reconecte para tentar novamente."
"pairingRefusedBody": "O Orca não conseguiu parear com ele novamente via SSH. Nada no host foi interrompido. Reconecte para tentar novamente.",
"signInTitle": "Aguardando login SSH",
"signInBody": "Este terminal continua sozinho quando você entrar no host.",
"signInButton": "Entrar"
},
"TerminalPane": {
"ac112e9036": "Remover título",
+4 -1
View File
@@ -3511,7 +3511,10 @@
"disconnectedTitle": "远程运行时已断开连接",
"retryingBody": "Orca 正在自动重试。如果连接恢复,此终端将恢复。",
"disconnectedBody": "自动重试已停止。重新连接以恢复此终端会话。",
"reconnectButton": "重新连接"
"reconnectButton": "重新连接",
"signInTitle": "正在等待 SSH 登录",
"signInBody": "登录到主机后,此终端会自动继续。",
"signInButton": "登录"
},
"TerminalLinkActionPopover": {
"openLink": "打开链接",