Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 14 additions & 31 deletions src/renderer/src/components/NewWorkspaceComposerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import type { TaskSourceContext } from '../../../shared/task-source-context'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import { unwrapRuntimeRpcResult } from '@/runtime/runtime-rpc-client'
import { translate } from '@/i18n/i18n'
import { withUiConnectTimeout } from '@/ssh/ssh-connect-ui-timeout'
import { isSshConnectInFlight, trackSshConnect } from '@/ssh/ssh-connect-in-flight'

type RepoOption = React.ComponentProps<typeof RepoCombobox>['repos'][number]
type EphemeralVmRecipeOption = NonNullable<OrcaHooks['environmentRecipes']>[number]
Expand Down Expand Up @@ -210,33 +212,6 @@ function getSshStatusLabel(status: SshConnectionStatus): string {
return SSH_STATUS_LABELS[status] ?? status
}

// Why: bound how long the run-target picker waits on a host connect so a stalled backend
// connect can't leave the row's disabled/spinner state stuck forever. The backend keeps going.
const RUN_TARGET_CONNECT_UI_TIMEOUT_MS = 20_000

async function withUiConnectTimeout<T>(promise: Promise<T>): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(() => {
reject(
new Error(
translate(
'auto.components.NewWorkspaceComposerCard.connectTimedOut',
'Connection timed out. It may still be connecting in the background.'
)
)
)
}, RUN_TARGET_CONNECT_UI_TIMEOUT_MS)
})
try {
return await Promise.race([promise, timeout])
} finally {
if (timer) {
clearTimeout(timer)
}
}
}

function SetupCommandPreview({ setupConfig }: { setupConfig: SetupConfig }): React.JSX.Element {
// Why: just the script in a quiet monochrome card — the source label (orca.yaml / local) and
// the run-setup toggle live in the section header above, so the card carries no chrome of its
Expand Down Expand Up @@ -534,10 +509,18 @@ export default function NewWorkspaceComposerCard({
}
try {
if (action.kind === 'ssh') {
// Why: ssh.connect has no built-in timeout; a stalled connect would otherwise leave the
// row's spinner/disabled state stuck forever. Bound the UI wait — the backend keeps
// connecting and the picker updates from store SSH state if it later succeeds.
await withUiConnectTimeout(window.api.ssh.connect({ targetId: action.targetId }))
if (isSshConnectInFlight(action.targetId)) {
return
}
// Why: ssh.connect has no built-in timeout; a stalled connect would otherwise leave
// the row's spinner/disabled state stuck forever. Bound the UI wait — the backend
// keeps connecting and the picker updates from store SSH state if it later succeeds.
// The shared registry tracks that backend request (not this bounded wait), so the
// sidebar card control and terminal overlay for this host stay locked until it
// settles — a second dial on a passphrase-gated target means a second prompt.
await withUiConnectTimeout(
trackSshConnect(action.targetId, window.api.ssh.connect({ targetId: action.targetId }))
)
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
ExternalAutomationProvider
} from '../../../../shared/automations-types'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import { isConnectingSshStatus } from '@/ssh/ssh-connection-recoverability'

export type ExternalAutomationSourceAvailability = {
statusLabel: string
Expand Down Expand Up @@ -75,7 +76,7 @@ export function getExternalAutomationSourceAvailability({
}

export function isSshConnectionBusy(status: SshConnectionStatus | undefined): boolean {
return status === 'connecting' || status === 'deploying-relay' || status === 'reconnecting'
return isConnectingSshStatus(status)
}

export function getExternalAutomationActionDisabledMessage(args: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { isConnectingSshStatus } from '@/ssh/ssh-connection-recoverability'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'

export type SshTargetBusyAction = 'terminate' | 'reset' | 'remove'

const SSH_TARGET_CONNECTING_STATUSES: ReadonlySet<SshConnectionStatus> = new Set([
'connecting',
'deploying-relay',
'reconnecting'
])

export function isSshTargetConnecting(status: SshConnectionStatus): boolean {
return SSH_TARGET_CONNECTING_STATUSES.has(status)
return isConnectingSshStatus(status)
}

export function shouldClearPendingSshReset({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type AutoRenameFailedDialogProps = {
* CLI output when main still holds it (in-memory, lost on restart), falling
* back to the persisted excerpt — either can run many lines, so it gets a
* dedicated scrollable surface rather than a tooltip — see the sibling
* SshDisconnectedDialog pattern.
* AddRemoteHostDialog pattern.
*/
export function AutoRenameFailedDialog({
open,
Expand Down Expand Up @@ -150,7 +150,7 @@ export function AutoRenameFailedDialog({
</div>
<DialogFooter>
{/* Why: Close backs the user out, so it stays quiet (outline, not a
solid CTA) — matching the sibling SshDisconnectedDialog. */}
solid CTA) — matching the other sidebar dialogs. */}
<Button type="button" variant="outline" size="sm" onClick={() => onOpenChange(false)}>
{translate('auto.components.sidebar.AutoRenameFailedDialog.aed1623b1e', 'Close')}
</Button>
Expand Down
11 changes: 6 additions & 5 deletions src/renderer/src/components/sidebar/HostSectionHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { useMountedRef } from '@/hooks/useMountedRef'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import { sshConnectVerb } from '@/ssh/ssh-connect-verb'
import { parseExecutionHostId } from '../../../../shared/execution-host'
import { describeRuntimeCompatBlock } from '../../../../shared/protocol-compat'
import {
Expand Down Expand Up @@ -74,18 +75,18 @@ export function HostSectionHeaderMenu({ row }: { row: HostHeaderRow }): React.JS
const [renameOpen, setRenameOpen] = useState(false)
const [removeOpen, setRemoveOpen] = useState(false)
const mountedRef = useMountedRef()
const sshConnected = useAppStore((s) => {
const sshStatus = useAppStore((s) => {
const parsed = parseExecutionHostId(row.hostId)
if (parsed?.kind !== 'ssh') {
return false
return null
}
return s.sshConnectionStates.get(parsed.targetId)?.status === 'connected'
return s.sshConnectionStates.get(parsed.targetId)?.status ?? null
})

const model = buildHostHeaderMenuModel({
kind: row.kind,
health: row.health,
sshConnected,
sshConnected: sshStatus === 'connected',
compatibility: row.compatibility
})
const removalTarget = resolveHostRemoval(row.hostId)
Expand Down Expand Up @@ -242,7 +243,7 @@ export function HostSectionHeaderMenu({ row }: { row: HostHeaderRow }): React.JS
{model.actions.includes('ssh-reconnect') && (
<DropdownMenuItem onSelect={() => void runSshAction('connect')}>
<Plug className="size-3.5" />
{translate('auto.components.sidebar.HostSectionHeaderMenu.63f36455cc', 'Reconnect')}
{sshConnectVerb(sshStatus)}
</DropdownMenuItem>
)}
{model.actions.includes('ssh-disconnect') && (
Expand Down
205 changes: 0 additions & 205 deletions src/renderer/src/components/sidebar/SshDisconnectedDialog.tsx

This file was deleted.

Loading