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