Files
orca/src/preload/api/ssh-api.ts
T
Neil 0b28553093 feat(ssh): show saved port forwards that couldn't start, with why, and let you remove them (#33450)
* feat(ssh): list saved port forwards that couldn't start, with why, and let you remove them (#32484)

A saved forward whose local port is taken, or that keeps failing, stayed
saved but invisible, retried in the background with no way to remove it.
The port-forward lists now show each saved forward that isn't open, with
the last start error, and Remove deletes it by its mapping.

* fix(ssh): a forward whose ssh exits after starting backs off like a failed start

* fix(ssh): re-register the new forward channels, close a forward a restore reopened before removing it, and update IPC fixtures

* fix(ssh): a flapping forward keeps its backoff streak until it stays up

* fix(ssh): translate the saved-forward row, say Stopped for a forward that died, toast a failed remove, and reset backoff on manual edits

* fix(ssh): keep saved forwards listed and removable while the host is offline; label the remove button; test the healthy-uptime reset

* fix(ssh): mirror saved forwards from main with a stale-read guard, including hosts that never connect

* fix(ssh): scroll the offline ports panel when saved forwards overflow it

* test(e2e): give the saved-forward restart case's timeout its reason
2026-10-25 02:37:30 -07:00

90 lines
3.4 KiB
TypeScript

import type {
EnrichedDetectedPort,
PortForwardEntry,
UnrestoredPortForward,
SshConfigHostListArgs,
SshConfigHostListResult,
SshConfigHostResolution,
SshConfigImportResult,
SshConnectionState,
SshTarget,
SshTargetAddResult,
SshTargetCreateInput,
SshTargetUpdateInput
} from '../../shared/ssh-types'
import type { FilesystemPathFlavor } from '../../shared/filesystem-entry-types'
export type SshApi = {
listTargets: () => Promise<SshTarget[]>
// Last known labels keyed by removed target ID, for workspaces still pinned to them.
listRemovedTargetLabels: () => Promise<Record<string, string>>
addTarget: (args: { target: SshTargetCreateInput }) => Promise<SshTargetAddResult>
updateTarget: (args: { id: string; updates: SshTargetUpdateInput }) => Promise<SshTarget>
removeTarget: (args: { id: string }) => Promise<void>
importConfig: (args?: { reAdopt?: boolean }) => Promise<SshConfigImportResult>
listConfigHosts: (args?: SshConfigHostListArgs) => Promise<SshConfigHostListResult>
resolveConfigHost: (args: { alias: string }) => Promise<SshConfigHostResolution | null>
/** `automatic`: a connect no user asked for; it never lifts a cancelled sign-in. */
connect: (args: { targetId: string; automatic?: boolean }) => Promise<SshConnectionState | null>
disconnect: (args: { targetId: string }) => Promise<void>
getState: (args: { targetId: string }) => Promise<SshConnectionState | null>
needsPassphrasePrompt: (args: { targetId: string }) => Promise<boolean>
testConnection: (args: {
targetId: string
}) => Promise<{ success: boolean; error?: string; state?: SshConnectionState }>
onStateChanged: (
callback: (data: { targetId: string; state: SshConnectionState }) => void
) => () => void
addPortForward: (args: {
targetId: string
localPort: number
remoteHost: string
remotePort: number
label?: string
}) => Promise<PortForwardEntry>
updatePortForward: (args: {
id: string
targetId: string
localPort: number
remoteHost: string
remotePort: number
label?: string
}) => Promise<PortForwardEntry>
removePortForward: (args: { id: string }) => Promise<PortForwardEntry | null>
listPortForwards: (args?: { targetId?: string }) => Promise<PortForwardEntry[]>
listUnrestoredPortForwards: (args: { targetId: string }) => Promise<UnrestoredPortForward[]>
removeSavedPortForward: (args: {
targetId: string
localPort: number
remoteHost: string
remotePort: number
}) => Promise<void>
listDetectedPorts: (args: { targetId: string }) => Promise<EnrichedDetectedPort[]>
onPortForwardsChanged: (
callback: (data: {
targetId: string
forwards: PortForwardEntry[]
unrestored?: UnrestoredPortForward[]
}) => void
) => () => void
onDetectedPortsChanged: (
callback: (data: { targetId: string; ports: EnrichedDetectedPort[] }) => void
) => () => void
browseDir: (args: { targetId: string; dirPath: string }) => Promise<{
entries: { name: string; isDirectory: boolean }[]
resolvedPath: string
pathFlavor: FilesystemPathFlavor
}>
onCredentialRequest: (
callback: (data: {
requestId: string
targetId: string
kind: 'passphrase' | 'password' | 'keyboard-interactive'
detail: string
echo?: boolean
}) => void
) => () => void
onCredentialResolved: (callback: (data: { requestId: string }) => void) => () => void
submitCredential: (args: { requestId: string; value: string | null }) => Promise<void>
}