Quiet disconnected SSH polling

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-13 02:53:42 -07:00
co-authored by Orca
parent 0fe7b4d41b
commit b97e48507e
6 changed files with 88 additions and 1 deletions
+25
View File
@@ -589,6 +589,31 @@ describe('preflight', () => {
})
})
it('returns no remote agents when the SSH connection is unavailable', async () => {
getActiveMultiplexerMock.mockReturnValue(null)
registerPreflightHandlers()
await expect(
handlers['preflight:detectRemoteAgents'](undefined, { connectionId: 'ssh-1' })
).resolves.toEqual([])
})
it('returns no remote agents when the SSH connection is disposed', async () => {
const request = vi.fn()
getActiveMultiplexerMock.mockReturnValue({
isDisposed: () => true,
request
})
registerPreflightHandlers()
await expect(
handlers['preflight:detectRemoteAgents'](undefined, { connectionId: 'ssh-1' })
).resolves.toEqual([])
expect(request).not.toHaveBeenCalled()
})
it('detects agents from the selected WSL distro for a WSL workspace', async () => {
Object.defineProperty(process, 'platform', {
configurable: true,
+3 -1
View File
@@ -248,7 +248,9 @@ export async function refreshShellPathAndDetectAgents(
export async function detectRemoteAgents(args: { connectionId: string }): Promise<string[]> {
const mux = getActiveMultiplexer(args.connectionId)
if (!mux || mux.isDisposed()) {
throw new Error(`No active SSH connection for "${args.connectionId}"`)
// Why: remote agent detection is passive UI polling. A disconnected host has
// no detectable agents until reconnect, but should not spam IPC errors.
return []
}
const result = (await mux.request('preflight.detectAgents', {
commands: KNOWN_AGENT_COMMANDS
+37
View File
@@ -2831,6 +2831,43 @@ describe('registerPtyHandlers', () => {
expect(store.markSshRemotePtyLease).toHaveBeenCalledWith('ssh-1', 'remote-pty', 'terminated')
})
it('returns idle process inspection results for detached SSH PTYs without a provider', async () => {
const provider = {
spawn: vi.fn(),
write: vi.fn(),
resize: vi.fn(),
shutdown: vi.fn(),
sendSignal: vi.fn(),
getCwd: vi.fn(),
getInitialCwd: vi.fn(),
clearBuffer: vi.fn(),
acknowledgeDataEvent: vi.fn(),
onData: vi.fn(() => () => {}),
onReplay: vi.fn(() => () => {}),
onExit: vi.fn(() => () => {}),
listProcesses: vi.fn(),
hasChildProcesses: vi.fn(),
getForegroundProcess: vi.fn(),
serialize: vi.fn(),
revive: vi.fn(),
getDefaultShell: vi.fn(),
getProfiles: vi.fn()
}
registerSshPtyProvider('ssh-1', provider as never)
registerPtyHandlers(mainWindow as never)
setPtyOwnership('remote-pty', 'ssh-1')
unregisterSshPtyProvider('ssh-1')
await expect(handlers.get('pty:hasChildProcesses')!(null, { id: 'remote-pty' })).resolves.toBe(
false
)
await expect(
handlers.get('pty:getForegroundProcess')!(null, { id: 'remote-pty' })
).resolves.toBeNull()
expect(provider.hasChildProcesses).not.toHaveBeenCalled()
expect(provider.getForegroundProcess).not.toHaveBeenCalled()
})
it('injects ORCA_TERMINAL_HANDLE for non-local PTY providers', async () => {
const spawn = vi.fn(async () => ({ id: 'remote-pty' }))
registerSshPtyProvider('ssh-1', {
+13
View File
@@ -226,6 +226,13 @@ function getProviderForPty(ptyId: string): IPtyProvider {
return getProvider(connectionId)
}
function hasPtyProviderForInspection(ptyId: string): boolean {
// Why: process inspection is background polling; disconnected SSH hosts should
// read as idle instead of surfacing repeated IPC errors.
const connectionId = ptyOwnership.get(ptyId)
return connectionId == null || sshProviders.has(connectionId)
}
function getAppPtyId(connectionId: string | null | undefined, ptyId: string): string {
return connectionId ? toAppSshPtyId(connectionId, ptyId) : ptyId
}
@@ -2801,6 +2808,9 @@ export function registerPtyHandlers(
ipcMain.handle(
'pty:hasChildProcesses',
async (_event, args: { id: string }): Promise<boolean> => {
if (!hasPtyProviderForInspection(args.id)) {
return false
}
return getProviderForPty(args.id).hasChildProcesses(args.id)
}
)
@@ -2808,6 +2818,9 @@ export function registerPtyHandlers(
ipcMain.handle(
'pty:getForegroundProcess',
async (_event, args: { id: string }): Promise<string | null> => {
if (!hasPtyProviderForInspection(args.id)) {
return null
}
return getProviderForPty(args.id).getForegroundProcess(args.id)
}
)