From d9aa941f6f67507ae073d71a3372c8f91eb73eb5 Mon Sep 17 00:00:00 2001
From: Neil <4138956+nwparker@users.noreply.github.com>
Date: Wed, 28 Oct 2026 12:40:56 -0700
Subject: [PATCH] fix(ssh): offer Remove from Orca for SSH workspaces this app
cannot delete (#35271)
* fix(ssh): offer Remove from Orca for SSH workspaces this app cannot delete
Refs #34677
* fix(ssh): retire delete-remote copy for unserved SSH workspaces
Refs #34677
* fix(ssh): retire delete-remote copy in the German catalog
Refs #34677
* fix(ssh): drop a stale host-removal comment
Refs #34677
* test(ssh): pin that an unserved SSH twin is only forgotten
Refs #34677
* test(ssh): build the unserved-twin store without a cast
Refs #34677
* test(ssh): type the unserved-twin store state
Refs #34677
---
config/scripts/locale-ko-key-overrides.json | 3 -
...worktrees-orphan-directory-cleanup.test.ts | 2 +-
.../removal/execute-worktree-removal.ts | 7 +-
.../removal/nested-worktree-removal.ts | 7 +-
.../providers/ssh-host-not-served-error.ts | 4 +
...removal-and-reconciliation-part-04.spec.ts | 2 +-
.../worktree-removal-execution-host.spec.ts | 2 +-
.../worktree-removal-execution-host-route.ts | 7 +-
.../src/components/settings/SshPane.tsx | 3 +-
.../settings/use-ssh-bulk-remove.ts | 3 +-
.../sidebar/ForgetSshWorkspaceDialog.test.tsx | 56 ++-------
.../sidebar/ForgetSshWorkspaceDialog.tsx | 119 +++---------------
.../components/sidebar/HostRemoveDialog.tsx | 38 ++----
.../sidebar/delete-worktree-flow.test.ts | 21 ++++
.../sidebar/delete-worktree-flow.ts | 14 ++-
.../ssh-host-remove-resolution.test.ts | 25 +---
.../sidebar/ssh-host-remove-resolution.ts | 11 +-
.../sidebar/ssh-host-remove-workspaces.ts | 17 +--
.../ssh-workspace-forget-resolution.test.ts | 85 +++++--------
.../ssh-workspace-forget-resolution.ts | 46 +++----
...worktree-delete-host-qualification.test.ts | 49 ++++----
.../src/i18n/en-runtime-required.json | 2 -
src/renderer/src/i18n/locales/de.json | 9 +-
src/renderer/src/i18n/locales/en.json | 9 +-
src/renderer/src/i18n/locales/es.json | 9 +-
src/renderer/src/i18n/locales/fr.json | 9 +-
src/renderer/src/i18n/locales/ja.json | 9 +-
src/renderer/src/i18n/locales/ko.json | 9 +-
src/renderer/src/i18n/locales/pt.json | 9 +-
src/renderer/src/i18n/locales/ru.json | 9 +-
src/renderer/src/i18n/locales/tr.json | 9 +-
src/renderer/src/i18n/locales/zh-Hant.json | 9 +-
src/renderer/src/i18n/locales/zh.json | 9 +-
...-active-worktree-removal-successor.test.ts | 9 +-
34 files changed, 188 insertions(+), 443 deletions(-)
diff --git a/config/scripts/locale-ko-key-overrides.json b/config/scripts/locale-ko-key-overrides.json
index 5a6f18751bb..7e1e8db7491 100644
--- a/config/scripts/locale-ko-key-overrides.json
+++ b/config/scripts/locale-ko-key-overrides.json
@@ -3533,9 +3533,6 @@
"auto.components.sidebar.DeleteWorktreeWarningPanels.c4f96a6e18": {
"ko": "메인 워크트리"
},
- "auto.components.sidebar.ForgetSshWorkspaceDialog.disconnectedBody": {
- "ko": "이 워크스페이스의 SSH 호스트가 연결되어 있지 않습니다. 재연결하여 원격에서도 삭제하거나 Orca에서만 제거하세요."
- },
"auto.components.sidebar.HostRemoveDialog.1a2b3c4d5e": {
"ko": "{{value0}} 제거됨"
},
diff --git a/src/main/ipc/worktrees-orphan-directory-cleanup.test.ts b/src/main/ipc/worktrees-orphan-directory-cleanup.test.ts
index b23f53b9430..89786d667d7 100644
--- a/src/main/ipc/worktrees-orphan-directory-cleanup.test.ts
+++ b/src/main/ipc/worktrees-orphan-directory-cleanup.test.ts
@@ -389,7 +389,7 @@ describe('registerWorktreeHandlers', () => {
worktreeId: `${repo.id}::${localPath}`,
force: true
})
- ).rejects.toThrow('Remote connection dropped')
+ ).rejects.toThrow('isn’t served by Orca’s server')
await expect(lstat(localPath)).resolves.toBeTruthy()
expect(removeWorktreeMock).not.toHaveBeenCalled()
diff --git a/src/main/ipc/worktrees/removal/execute-worktree-removal.ts b/src/main/ipc/worktrees/removal/execute-worktree-removal.ts
index 76a9e857edf..93a31c76fe0 100644
--- a/src/main/ipc/worktrees/removal/execute-worktree-removal.ts
+++ b/src/main/ipc/worktrees/removal/execute-worktree-removal.ts
@@ -7,7 +7,10 @@ import { assertWorktreeUnlockedForRemoval } from '../../../../shared/worktree/re
import { isWindowsAbsolutePathLike } from '../../../../shared/cross-platform-path'
import { getLocalProjectWorktreeGitOptions } from '../../../project-runtime-git-options'
import { listWorktreesStrict as listGitWorktreesStrict } from '../../../git/worktree'
-import { SshHostNotServedError } from '../../../providers/ssh-host-not-served-error'
+import {
+ SshHostNotServedError,
+ SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE
+} from '../../../providers/ssh-host-not-served-error'
import { resolveWorktreeRemovalMetadata } from '../../../worktree-removal-repo-owner'
import {
authorizeUnregisteredWindowsRemoval,
@@ -54,7 +57,7 @@ export async function executeWorktreeRemoval(
}
assertRemovalHostMatchesRepoRow(repo, repoId, removalHostId)
if (repo.connectionId) {
- throw new SshHostNotServedError(repo.connectionId)
+ throw new SshHostNotServedError(repo.connectionId, SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE)
}
const localWorktreeGitOptions = getLocalProjectWorktreeGitOptions(store, repo)
const hasLocalWorktreeGitOptions = Object.keys(localWorktreeGitOptions).length > 0
diff --git a/src/main/ipc/worktrees/removal/nested-worktree-removal.ts b/src/main/ipc/worktrees/removal/nested-worktree-removal.ts
index 76c05986b25..2d4e23ec81b 100644
--- a/src/main/ipc/worktrees/removal/nested-worktree-removal.ts
+++ b/src/main/ipc/worktrees/removal/nested-worktree-removal.ts
@@ -5,7 +5,10 @@ import type { RemoveWorktreeResult } from '../../../../shared/worktree/create-ty
import { isFolderRepo } from '../../../../shared/repo-kind'
import { listWorktreesStrict } from '../../../git/worktree'
import { getLocalProjectWorktreeGitOptions } from '../../../project-runtime-git-options'
-import { SshHostNotServedError } from '../../../providers/ssh-host-not-served-error'
+import {
+ SshHostNotServedError,
+ SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE
+} from '../../../providers/ssh-host-not-served-error'
import { resolveWorktreeRemovalHomeForHost } from '../../../worktree-removal-execution-host-route'
import {
getNestedWorktreeRemovalPlan,
@@ -26,7 +29,7 @@ export async function previewNestedWorktreeRemoval(
}
assertRemovalHostMatchesRepoRow(repo, repo.id, hostId)
if (repo.connectionId) {
- throw new SshHostNotServedError(repo.connectionId)
+ throw new SshHostNotServedError(repo.connectionId, SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE)
}
const worktrees = await listWorktreesStrict(
repo.path,
diff --git a/src/main/providers/ssh-host-not-served-error.ts b/src/main/providers/ssh-host-not-served-error.ts
index 5b6111b3164..3d7ab42ed19 100644
--- a/src/main/providers/ssh-host-not-served-error.ts
+++ b/src/main/providers/ssh-host-not-served-error.ts
@@ -2,6 +2,10 @@
export const SSH_HOST_NOT_SERVED_MESSAGE =
'Remote connection dropped. Click Reconnect on the SSH target before retrying.'
+/** Reconnecting cannot help: only the host's Orca server could delete there, and it lacks this project. */
+export const SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE =
+ 'Orca can’t delete this workspace on its SSH host because its project isn’t served by Orca’s server there. Remove it from Orca instead; files on the host are left untouched.'
+
export const SSH_GIT_RECONNECT_MESSAGE =
'SSH Git provider is not available. Reconnect to this target and try again.'
diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts
index b3b74e89f39..e74795aba93 100644
--- a/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts
+++ b/src/main/runtime/orca-runtime-tests/worktree-removal-and-reconciliation-part-04.spec.ts
@@ -86,7 +86,7 @@ describe('OrcaRuntimeService', () => {
try {
await expect(
runtime.removeManagedWorktree(`id:${worktreeId}`, { force: true })
- ).rejects.toThrow('Remote connection dropped')
+ ).rejects.toThrow('isn’t served by Orca’s server')
await expect(lstat(localPath)).resolves.toBeTruthy()
expect(removeWorktree).not.toHaveBeenCalled()
diff --git a/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts
index 28030090b17..8b6306a32ff 100644
--- a/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts
+++ b/src/main/runtime/orca-runtime-tests/worktree-removal-execution-host.spec.ts
@@ -85,7 +85,7 @@ describe('OrcaRuntimeService worktree removal execution host', () => {
allowUnverifiedPtyStop: false,
hostId: 'ssh:target-a'
})
- ).rejects.toThrow('Remote connection dropped')
+ ).rejects.toThrow('isn’t served by Orca’s server')
expect(listWorktreesStrict).not.toHaveBeenCalled()
expect(removeWorktree).not.toHaveBeenCalled()
diff --git a/src/main/worktree-removal-execution-host-route.ts b/src/main/worktree-removal-execution-host-route.ts
index 6a4a1e06b24..b311adfad8e 100644
--- a/src/main/worktree-removal-execution-host-route.ts
+++ b/src/main/worktree-removal-execution-host-route.ts
@@ -14,7 +14,10 @@ import {
ExecutionHostNotDispatchableError,
resolveGitRouteForHost
} from './providers/execution-host-provider-dispatch'
-import { SshHostNotServedError } from './providers/ssh-host-not-served-error'
+import {
+ SshHostNotServedError,
+ SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE
+} from './providers/ssh-host-not-served-error'
export type WorktreeRemovalRoute = { kind: 'local'; hostId: typeof LOCAL_EXECUTION_HOST_ID }
@@ -26,7 +29,7 @@ export function resolveWorktreeRemovalRoute(hostId: ExecutionHostId): WorktreeRe
case 'runtime':
throw new ExecutionHostNotDispatchableError(route.hostId)
case 'ssh':
- throw new SshHostNotServedError(route.connectionId)
+ throw new SshHostNotServedError(route.connectionId, SSH_WORKTREE_REMOVAL_NOT_SERVED_MESSAGE)
}
}
diff --git a/src/renderer/src/components/settings/SshPane.tsx b/src/renderer/src/components/settings/SshPane.tsx
index e177e9d7442..13ec4d91019 100644
--- a/src/renderer/src/components/settings/SshPane.tsx
+++ b/src/renderer/src/components/settings/SshPane.tsx
@@ -177,8 +177,7 @@ export function SshPane({ addTargetIntentSignal }: SshPaneProps): React.JSX.Elem
const resolution = resolveSshHostRemoval({
targetId: target.id,
repos: useAppStore.getState().repos,
- worktrees: getAllWorktreesFromState(useAppStore.getState()),
- sshConnectionStates: useAppStore.getState().sshConnectionStates
+ worktrees: getAllWorktreesFromState(useAppStore.getState())
})
if (resolution.workspaceCount > 0) {
setHostRemoveTarget({ targetId: target.id, label: target.label })
diff --git a/src/renderer/src/components/settings/use-ssh-bulk-remove.ts b/src/renderer/src/components/settings/use-ssh-bulk-remove.ts
index 2c99e860165..33b4d1bbd47 100644
--- a/src/renderer/src/components/settings/use-ssh-bulk-remove.ts
+++ b/src/renderer/src/components/settings/use-ssh-bulk-remove.ts
@@ -80,8 +80,7 @@ export function useSshBulkRemove(args: {
const resolution = resolveSshHostRemoval({
targetId: target.id,
repos: state.repos,
- worktrees,
- sshConnectionStates: state.sshConnectionStates
+ worktrees
})
return resolution.workspaceCount > 0 ? 'workspaces' : null
})
diff --git a/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.test.tsx b/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.test.tsx
index 244c1df84b8..c79c166771e 100644
--- a/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.test.tsx
+++ b/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.test.tsx
@@ -5,7 +5,6 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
import type { ButtonHTMLAttributes, ReactNode } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Worktree } from '../../../../shared/worktree/types'
-import type { RemoveWorktreeOptions } from '@/store/slices/worktree-removal-options'
const mocks = vi.hoisted(() => {
const allWorktrees: Worktree[] = []
@@ -21,7 +20,7 @@ const mocks = vi.hoisted(() => {
gitStatusByWorktree: {},
deleteStateByWorktreeId: {}
}
- return { state, connect: vi.fn() }
+ return { state }
})
vi.mock('@/store', () => ({
@@ -60,10 +59,6 @@ vi.mock('./removal-unsaved-files', () => ({ blockRemovalForUnsavedFiles: () => f
vi.mock('@/store/slices/worktrees/teardown/removed-workspace-unsaved-files', () => ({
confirmUnsavedDiscard: () => new Map()
}))
-vi.mock('./delete-worktree-flow', async () => ({
- runWorktreeDeleteWithToast: (await import('./run-worktree-delete-with-toast'))
- .runWorktreeDeleteWithToast
-}))
import ForgetSshWorkspaceDialog from './ForgetSshWorkspaceDialog'
@@ -93,63 +88,34 @@ function sshWorkspace(instanceId: string): Worktree {
}
}
-function replaceWorkspace(): void {
- mocks.state.allWorktrees.splice(0, 1, sshWorkspace('instance-B'))
-}
-
-describe('ForgetSshWorkspaceDialog Reconnect & Delete (STA-11912)', () => {
+describe('ForgetSshWorkspaceDialog', () => {
beforeEach(() => {
mocks.state.allWorktrees.splice(0, mocks.state.allWorktrees.length, sshWorkspace('instance-A'))
mocks.state.modalData = {
worktreeId: 'repo::/srv/wt',
worktreeDeleteIdentities: [{ id: 'repo::/srv/wt', instanceId: 'instance-A', hostId }],
displayName: 'wt',
- resolution: { kind: 'disconnected', targetId: 'box' }
+ resolution: { kind: 'saved', targetId: 'box' }
}
- mocks.state.removeWorktree.mockReset()
+ mocks.state.removeWorktree.mockReset().mockResolvedValue({ ok: true })
mocks.state.closeModal.mockReset()
- mocks.state.clearWorktreeDeleteState.mockReset()
- mocks.connect.mockReset()
- vi.stubGlobal('api', { ssh: { connect: mocks.connect } })
})
afterEach(() => {
cleanup()
- vi.unstubAllGlobals()
})
- it('refuses when a replacement appears at the same host and id during reconnect', async () => {
- mocks.connect.mockImplementation(async () => replaceWorkspace())
+ it('forgets a saved host’s workspace on its SSH host without a remote delete', async () => {
render()
- fireEvent.click(screen.getByText('Reconnect & Delete'))
+ fireEvent.click(screen.getByText('Remove from Orca'))
await waitFor(() => expect(mocks.state.closeModal).toHaveBeenCalled())
- expect(mocks.state.removeWorktree).not.toHaveBeenCalled()
- })
-
- it('re-checks the selected incarnation immediately before the destructive send', async () => {
- mocks.connect.mockResolvedValue(undefined)
- let dispatchError: unknown = null
- mocks.state.removeWorktree.mockImplementation(
- async (_target: unknown, _force: boolean, options?: RemoveWorktreeOptions) => {
- replaceWorkspace()
- try {
- options?.assertBeforeDispatch?.()
- } catch (error) {
- dispatchError = error
- return { ok: false, error: String(error) }
- }
- return { ok: true }
- }
+ expect(mocks.state.removeWorktree).toHaveBeenCalledTimes(1)
+ expect(mocks.state.removeWorktree).toHaveBeenCalledWith(
+ { id: 'repo::/srv/wt', executionHostId: hostId },
+ false,
+ expect.objectContaining({ mode: 'forget-local' })
)
- render()
-
- fireEvent.click(screen.getByText('Reconnect & Delete'))
-
- await waitFor(() =>
- expect(mocks.state.clearWorktreeDeleteState).toHaveBeenCalledWith('repo::/srv/wt', hostId)
- )
- expect(dispatchError).toBeInstanceOf(Error)
})
})
diff --git a/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.tsx b/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.tsx
index c4f9e61e877..fdc4d5e86c8 100644
--- a/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.tsx
+++ b/src/renderer/src/components/sidebar/ForgetSshWorkspaceDialog.tsx
@@ -13,7 +13,6 @@ import { Button } from '@/components/ui/button'
import { useMountedRef } from '@/hooks/useMountedRef'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
-import { runWorktreeDeleteWithToast } from './delete-worktree-flow'
import { toSshExecutionHostId } from '../../../../shared/execution-host'
import type { WorktreeRemovalTarget } from '../../../../shared/worktree/removal'
import type { SshWorkspaceForgetResolution } from './ssh-workspace-forget-resolution'
@@ -53,7 +52,7 @@ export function ForgetSshWorkspaceDialog(): React.JSX.Element | null {
// Retain removed-host labels for ghost workspaces before falling back to raw IDs.
return s.sshTargetLabels.get(targetId) ?? s.removedSshTargetLabels.get(targetId) ?? targetId
})
- const [busy, setBusy] = useState(null)
+ const [busy, setBusy] = useState(false)
const mountedRef = useMountedRef()
const unsavedTargets = useMemo(
() =>
@@ -72,7 +71,6 @@ export function ForgetSshWorkspaceDialog(): React.JSX.Element | null {
return null
}
const { worktreeId, displayName, resolution } = modalData
- const canReconnect = resolution.kind === 'disconnected'
// This workspace’s named SSH target is the host confirmed for removal.
const removalTarget: WorktreeRemovalTarget = {
id: worktreeId,
@@ -95,73 +93,18 @@ export function ForgetSshWorkspaceDialog(): React.JSX.Element | null {
const refuseReplacedSelection = (): void => {
showWorkspaceListChangedToast()
if (mountedRef.current) {
- setBusy(null)
+ setBusy(false)
}
closeModal()
}
const done = (): void => {
if (mountedRef.current) {
- setBusy(null)
+ setBusy(false)
closeModal()
}
}
- const handleReconnectAndDelete = async (): Promise => {
- if (resolution.kind !== 'disconnected') {
- return
- }
- if (!isSelectedWorkspaceCurrent()) {
- refuseReplacedSelection()
- return
- }
- const confirmedDiscards = confirmUnsavedDiscard(useAppStore.getState, unsavedTargets)
- setBusy('reconnect')
- try {
- await window.api.ssh.connect({ targetId: resolution.targetId })
- } catch (err) {
- if (mountedRef.current) {
- setBusy(null)
- }
- toast.error(
- err instanceof Error
- ? err.message
- : translate(
- 'auto.components.sidebar.ForgetSshWorkspaceDialog.reconnectFailed',
- 'Reconnection failed'
- )
- )
- return
- }
- if (!isSelectedWorkspaceCurrent()) {
- refuseReplacedSelection()
- return
- }
- // Close before the delete toast to avoid overlapping confirmations.
- closeModal()
- void runWorktreeDeleteWithToast(removalTarget, displayName, {
- confirmedDiscards,
- assertBeforeDispatch: () => {
- if (!isSelectedWorkspaceCurrent()) {
- throw new Error(
- translate(
- 'auto.components.sidebar.delete.worktree.flow.workspaceListChanged',
- 'Workspace list changed'
- )
- )
- }
- }
- }).then((deleted) => {
- // Why: the refusal's error state is keyed by id and host, which the replacement row shares.
- if (!deleted && !isSelectedWorkspaceCurrent() && removalTarget.executionHostId) {
- useAppStore.getState().clearWorktreeDeleteState(worktreeId, removalTarget.executionHostId)
- }
- })
- if (mountedRef.current) {
- setBusy(null)
- }
- }
-
// Forget only Orca’s records; remote files, worktrees and branches survive.
const handleForget = async (): Promise => {
if (!isSelectedWorkspaceCurrent()) {
@@ -169,7 +112,7 @@ export function ForgetSshWorkspaceDialog(): React.JSX.Element | null {
return
}
const confirmedDiscards = confirmUnsavedDiscard(useAppStore.getState, unsavedTargets)
- setBusy('forget')
+ setBusy(true)
try {
const result = await useAppStore
.getState()
@@ -177,7 +120,7 @@ export function ForgetSshWorkspaceDialog(): React.JSX.Element | null {
if (!result.ok) {
toast.error(result.error)
if (mountedRef.current) {
- setBusy(null)
+ setBusy(false)
}
return
}
@@ -185,17 +128,11 @@ export function ForgetSshWorkspaceDialog(): React.JSX.Element | null {
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err))
if (mountedRef.current) {
- setBusy(null)
+ setBusy(false)
}
}
}
- const forgetDescription = translate(
- 'auto.components.sidebar.ForgetSshWorkspaceDialog.forgetBody',
- 'Removes this workspace from Orca only. Files, the Git worktree, and branches on {{host}} are left untouched.',
- { host: hostLabel }
- )
-
return (
diff --git a/src/renderer/src/components/sidebar/HostRemoveDialog.tsx b/src/renderer/src/components/sidebar/HostRemoveDialog.tsx
index 77524cf5608..fb56019ed3b 100644
--- a/src/renderer/src/components/sidebar/HostRemoveDialog.tsx
+++ b/src/renderer/src/components/sidebar/HostRemoveDialog.tsx
@@ -55,7 +55,6 @@ export function HostRemoveDialog({
const repos = useAppStore((s) => s.repos)
const worktreesByRepo = useAppStore((s) => selectWorktreesByRepo(s))
const detectedWorktreesByRepo = useAppStore((s) => selectDetectedWorktreesByRepo(s))
- const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
const sshResolution = useMemo(() => {
if (target.kind !== 'ssh') {
@@ -64,10 +63,9 @@ export function HostRemoveDialog({
return resolveSshHostRemoval({
targetId: target.targetId,
repos,
- worktrees: getAllWorktreesFromState({ worktreesByRepo }),
- sshConnectionStates
+ worktrees: getAllWorktreesFromState({ worktreesByRepo })
})
- }, [target, repos, worktreesByRepo, sshConnectionStates])
+ }, [target, repos, worktreesByRepo])
const unsavedTargets = useMemo(() => {
if (!sshResolution || !deleteWorkspaces) {
@@ -85,7 +83,6 @@ export function HostRemoveDialog({
const workspaceCount = sshResolution?.workspaceCount ?? 0
const hasWorkspaces = workspaceCount > 0
- const isConnected = sshResolution?.isConnected ?? false
// Drop orphaned label overrides so reused host IDs cannot inherit stale names.
const dropOverridesForHost = (): void => {
@@ -120,7 +117,6 @@ export function HostRemoveDialog({
if (target.kind === 'ssh' && deleteWorkspaces && sshResolution) {
const { failedIds } = await clearSshHostWorkspaces(
sshResolution,
- isConnected ? 'delete-remote' : 'forget-local',
confirmUnsavedDiscard(useAppStore.getState, unsavedTargets)
)
// Keep the host if workspace cleanup fails so stranded rows remain reachable for retry.
@@ -202,18 +198,11 @@ export function HostRemoveDialog({
'This removes the saved SSH host and its credentials from this computer. Remote files are not deleted.'
)
- // Connected hosts delete remote files; offline/ghost hosts only forget local records.
- const deleteOptionLabel = isConnected
- ? translate(
- 'auto.components.sidebar.HostRemoveDialog.alsoDeleteRemote',
- 'Also delete these {{value0}} on {{value1}}',
- { value0: workspaceCountLabel, value1: label }
- )
- : translate(
- 'auto.components.sidebar.HostRemoveDialog.alsoForgetLocal',
- 'Also remove these {{value0}} from Orca',
- { value0: workspaceCountLabel }
- )
+ const deleteOptionLabel = translate(
+ 'auto.components.sidebar.HostRemoveDialog.alsoForgetLocal',
+ 'Also remove these {{value0}} from Orca',
+ { value0: workspaceCountLabel }
+ )
return (