From 610e5bedf1d6eb99efd91e9e4a1a7239f9ec1807 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Sat, 4 Jul 2026 02:00:57 -0700
Subject: [PATCH] fix(ssh): make reconnection prompts non-blocking unless the
project is focused
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The SSH reconnect dialog was auto-opened at render time for any active/restored
worktree whose SSH target was disconnected, blocking the entire UI even when the
user was working elsewhere. On app restart the previously-active worktree is
restored without a click, so the modal stole focus app-wide over unrelated work.
The disconnected state is now surfaced only through the non-blocking affordances
that already existed — the worktree card chip, the status-bar Connect action, and
the pane-scoped terminal reconnect overlay. The blocking dialog opens only on
deliberate focus (clicking the worktree card), and even then it is suppressed when
the active view is a terminal, since that pane already shows the in-context
reconnect overlay and a second prompt would just duplicate it.
Fixes #7314
---
...WorktreeCard.ssh-reconnect-prompt.test.tsx | 10 +++---
.../src/components/sidebar/WorktreeCard.tsx | 33 ++++++++++---------
2 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx
index 9af104c6c18..4d0fdec107a 100644
--- a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx
@@ -133,7 +133,7 @@ describe('WorktreeCard SSH reconnect prompt', () => {
worktreeCardProperties = ['status']
})
- it('opens the reconnect dialog for an active disconnected SSH worktree during render', () => {
+ it('does not auto-open the blocking reconnect dialog for a restored active disconnected SSH worktree', () => {
sshConnectionStates.set('ssh-target-1', { status: 'disconnected' })
sshTargetLabels.set('ssh-target-1', 'Remote target')
@@ -141,9 +141,11 @@ describe('WorktreeCard SSH reconnect prompt', () => {
)
- expect(markup).toContain('data-ssh-disconnected-dialog="open"')
- expect(markup).toContain('data-ssh-status="disconnected"')
- expect(markup).toContain('data-ssh-target-label="Remote target"')
+ // The dialog is blocking, so being the active/restored card must not steal
+ // focus app-wide; it only opens on deliberate click (see handleClick).
+ expect(markup).toContain('data-ssh-disconnected-dialog="closed"')
+ // The disconnected state is still discoverable via the non-blocking card chip.
+ expect(markup).toContain('SSH disconnected')
})
it('marks a runtime-host worktree disconnected when its environment has no status', () => {
diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx
index a823442d23d..003878d3acf 100644
--- a/src/renderer/src/components/sidebar/WorktreeCard.tsx
+++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx
@@ -342,6 +342,13 @@ const WorktreeCard = React.memo(function WorktreeCard({
return state?.status ?? 'disconnected'
})
const isSshDisconnected = sshStatus != null && sshStatus !== 'connected'
+ // Why: a terminal view already carries its own in-pane reconnect overlay, so
+ // the blocking dialog would just duplicate it there; reserve the dialog for
+ // views without an in-context prompt. Default to terminal (suppress) for the
+ // ambiguous case so we err toward non-blocking.
+ const activeViewIsTerminal = useAppStore(
+ (s) => (s.activeTabTypeByWorktree?.[worktree.id] ?? 'terminal') === 'terminal'
+ )
// Why: runtime ("Orca server") hosts get the same disconnected treatment as
// SSH — when the host's runtime environment has no live status, its worktrees
@@ -353,23 +360,14 @@ const WorktreeCard = React.memo(function WorktreeCard({
}
return !s.runtimeStatusByEnvironmentId.get(parsed.environmentId)?.status
})
+ // Why: the reconnect dialog is blocking, so it is never auto-shown for a
+ // disconnected worktree just because it is the active/restored card — that
+ // would steal focus app-wide while the user works elsewhere. The card chip,
+ // status bar, and terminal overlay carry the non-blocking disconnected state;
+ // the dialog only opens on deliberate focus (see handleClick).
const [showDisconnectedDialog, setShowDisconnectedDialog] = useState(false)
- const sshDisconnectedPromptKey = isActive && isSshDisconnected ? worktree.id : null
- const [lastSshDisconnectedPromptKey, setLastSshDisconnectedPromptKey] = useState(
- null
- )
const [titleRenaming, setTitleRenaming] = useState(false)
const [showRenameErrorDialog, setShowRenameErrorDialog] = useState(false)
-
- // Why: on restart the previously-active worktree is auto-restored without a
- // click, so the dialog never opens. Auto-show it for the active card when SSH
- // is disconnected, but keep dismissals sticky until that prompt key changes.
- if (sshDisconnectedPromptKey !== lastSshDisconnectedPromptKey) {
- setLastSshDisconnectedPromptKey(sshDisconnectedPromptKey)
- if (sshDisconnectedPromptKey) {
- setShowDisconnectedDialog(true)
- }
- }
// Why: read the target label from the store (populated during hydration in
// useIpcEvents.ts) instead of calling listTargets IPC per card instance.
const sshTargetLabel = useAppStore((s) =>
@@ -862,7 +860,11 @@ const WorktreeCard = React.memo(function WorktreeCard({
})
onImmediateActivate?.(worktree.id, activationRowKey)
void activateWorktreeFromSidebar(worktree.id)
- if (isSshDisconnected) {
+ // Why: clicking the card is a deliberate focus of this project, so the
+ // blocking reconnect prompt is appropriate here (unlike auto-restore) —
+ // but skip it when a terminal is active, since that pane already shows the
+ // in-context reconnect overlay and a second prompt would just duplicate it.
+ if (isSshDisconnected && !activeViewIsTerminal) {
setShowDisconnectedDialog(true)
}
onActivate?.()
@@ -875,6 +877,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
isDeleting,
activationRowKey,
isSshDisconnected,
+ activeViewIsTerminal,
onActivate,
onImmediateActivate,
onSelectionGesture