mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
fix(ssh): make reconnection prompts non-blocking unless the project is focused
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
This commit is contained in:
@@ -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', () => {
|
||||
<WorktreeCard worktree={makeWorktree()} repo={makeRepo()} isActive={true} />
|
||||
)
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -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<string | null>(
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user