fix(terminal): make SSH reconnect UI non-blocking (#9928)

This commit is contained in:
Neil
2026-07-22 13:46:05 -07:00
committed by GitHub
parent 56a31a5af0
commit 53e018a1a6
2 changed files with 69 additions and 61 deletions
@@ -69,12 +69,12 @@ describe('TerminalSshReconnectOverlay', () => {
cleanup()
})
it('renders a direct Connect action for a disconnected SSH terminal', async () => {
it('renders a non-blocking Connect banner for a disconnected SSH terminal', async () => {
const connect = vi.fn().mockResolvedValue(undefined)
installSshConnect(connect)
const user = userEvent.setup()
render(
const { container } = render(
<TerminalSshReconnectOverlay
targetId="ssh-target-1"
targetLabel="devbox"
@@ -84,6 +84,10 @@ describe('TerminalSshReconnectOverlay', () => {
expect(screen.getByText('SSH connection required')).toBeInTheDocument()
expect(screen.getByText(/This terminal is waiting for devbox/)).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
const banner = container.querySelector('[data-terminal-ssh-reconnect-banner="disconnected"]')
expect(banner).toHaveClass('inset-x-3', 'bottom-3')
expect(banner).not.toHaveClass('inset-0', 'bg-background/75')
await user.click(screen.getByRole('button', { name: 'Connect' }))
expect(connect).toHaveBeenCalledWith({ targetId: 'ssh-target-1' })
@@ -138,20 +138,24 @@ export function TerminalSshReconnectOverlay({
return (
<div
className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center bg-background/75 px-6 py-8 backdrop-blur-[1px]"
data-terminal-ssh-reconnect-overlay="true"
className="pointer-events-none absolute inset-x-3 bottom-3 z-30 flex justify-center"
data-terminal-ssh-reconnect-banner={status}
>
<div className="pointer-events-auto flex w-full max-w-sm flex-col gap-3 rounded-md border border-border bg-card px-4 py-4 text-card-foreground shadow-xs">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-muted text-muted-foreground">
{isConnecting ? (
<Loader2 className="size-4 animate-spin" />
) : (
<ServerOff className="size-4" />
)}
</div>
<div className="min-w-0 space-y-1">
<div className="text-sm font-semibold">
<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]"
role="status"
aria-live="polite"
>
<div className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border bg-muted text-muted-foreground">
{isConnecting ? (
<Loader2 className="size-4 animate-spin" />
) : (
<ServerOff className="size-4" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<div className="shrink-0 text-sm font-semibold">
{targetRemoved
? translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.removedTitle',
@@ -162,56 +166,56 @@ export function TerminalSshReconnectOverlay({
'SSH connection required'
)}
</div>
<div className="text-xs leading-5 text-muted-foreground">
{targetRemoved
? translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.removedBody',
'The SSH host for this workspace was removed, so it can no longer connect. Remove the workspace to clear it — remote files are left untouched.'
)
: messageForStatus(status, targetLabel)}
<div className="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<Server className="size-3.5 shrink-0" />
<span className="truncate font-medium">{targetLabel}</span>
</div>
</div>
</div>
<div className="flex items-center justify-between gap-3 rounded-md border border-border/60 bg-muted/40 px-3 py-2">
<div className="flex min-w-0 items-center gap-2">
<Server className="size-3.5 shrink-0 text-muted-foreground" />
<span className="truncate text-xs font-medium">{targetLabel}</span>
</div>
{targetRemoved ? (
<Button
size="sm"
variant="outline"
onClick={worktreeId ? () => runWorktreeDelete(worktreeId) : undefined}
disabled={!worktreeId}
>
{translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.removeWorkspaceButton',
'Remove workspace'
)}
</Button>
) : (
<Button
size="sm"
onClick={showConnect ? () => void handleConnect() : undefined}
disabled={!showConnect || isConnecting}
>
{!showConnect || isConnecting ? (
<>
<Loader2 className="size-3.5 animate-spin" />
{translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.connectingButton',
'Connecting...'
)}
</>
) : (
translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.connectButton',
'Connect'
<div className="mt-0.5 text-xs leading-5 text-muted-foreground">
{targetRemoved
? translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.removedBody',
'The SSH host for this workspace was removed, so it can no longer connect. Remove the workspace to clear it — remote files are left untouched.'
)
)}
</Button>
)}
: messageForStatus(status, targetLabel)}
</div>
</div>
{targetRemoved ? (
<Button
className="shrink-0"
size="sm"
variant="outline"
onClick={worktreeId ? () => runWorktreeDelete(worktreeId) : undefined}
disabled={!worktreeId}
>
{translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.removeWorkspaceButton',
'Remove workspace'
)}
</Button>
) : (
<Button
className="shrink-0"
size="sm"
onClick={showConnect ? () => void handleConnect() : undefined}
disabled={!showConnect || isConnecting}
>
{!showConnect || isConnecting ? (
<>
<Loader2 className="size-3.5 animate-spin" />
{translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.connectingButton',
'Connecting...'
)}
</>
) : (
translate(
'auto.components.terminal.pane.TerminalSshReconnectOverlay.connectButton',
'Connect'
)
)}
</Button>
)}
</div>
</div>
)