Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/main/ipc/worktrees.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8380,6 +8380,8 @@ describe('registerWorktreeHandlers', () => {
connectionId: 'conn-1'
})
getSshPtyProviderMock.mockReturnValue(sshPtyProvider)
// One global meta key can describe the same-id local copy; the resolved repo still owns this delete.
store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ hostId: 'local' }))

await handlers['worktrees:remove'](null, { worktreeId })

Expand All @@ -8395,6 +8397,37 @@ describe('registerWorktreeHandlers', () => {
})
})

it('fences a mirrored runtime folder workspace sweep to its environment', async () => {
const runtimePtyProvider = {} as never
const worktreeId = 'repo-folder::/runtime/folder::workspace:child-1'
store.getRepo.mockReturnValue({
id: 'repo-folder',
path: '/runtime/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder',
executionHostId: 'runtime:env-1'
})
getLocalPtyProviderMock.mockReturnValue(runtimePtyProvider)

await handlers['worktrees:remove'](null, {
worktreeId,
hostId: 'runtime:env-1'
})

expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, {
runtime: runtimeStub,
resolvedWorktreeId: worktreeId,
resolvedRuntimeEnvironmentId: 'env-1',
localProvider: runtimePtyProvider,
onPtyStopped: clearProviderPtyStateMock,
includeProviderInventory: false,
includeLocalRegistry: false
})
expect(getSshPtyProviderMock).not.toHaveBeenCalled()
})

it('runs the archive hook on remove when skipArchive is not set', async () => {
mockKnownFeatureWorktree()
removeWorktreeMock.mockResolvedValue(undefined)
Expand Down
6 changes: 3 additions & 3 deletions src/main/ipc/worktrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2297,9 +2297,9 @@ export function registerWorktreeHandlers(
await withWorktreeRemoveStageSpan('pty_sweep', 'folder', async () => {
// Folder projects can be SSH-backed, so fence the sweep to the owning host exactly
// like the git paths — the local inventory must never reach a remote workspace's id.
const ownerHost = parseExecutionHostId(
resolveWorktreeRemovalOwnerHostId(store, args.worktreeId, repo, args.hostId)
)
// The resolved repo is authoritative here: path-derived metadata is shared by
// same-id host copies and can describe a different owner's workspace.
const ownerHost = parseExecutionHostId(removalHostId)
const sshPtyProvider =
ownerHost?.kind === 'ssh' ? getSshPtyProvider(ownerHost.targetId) : undefined
const externalHost = ownerHost?.kind === 'ssh' || ownerHost?.kind === 'runtime'
Expand Down
4 changes: 3 additions & 1 deletion src/main/runtime/orca-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3943,7 +3943,9 @@ describe('OrcaRuntimeService', () => {
displayName: 'Folder',
badgeColor: 'blue',
addedAt: 1,
kind: 'folder' as const
kind: 'folder' as const,
// removeManagedWorktree executes inside this selected runtime, where PTYs are local ids.
executionHostId: 'runtime:env-1' as const
}
const rootWorktreeId = 'folder-repo::/workspace/folder'
const rootPriorWorktreeIds = ['folder-repo::/workspace/old-folder']
Expand Down
24 changes: 9 additions & 15 deletions src/main/runtime/orca-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23511,31 +23511,25 @@ export class OrcaRuntimeService {
'Cannot delete the project root workspace. Remove the folder project instead.'
)
}
// Folder projects can be SSH-backed, so resolve the owner before sweeping.
const folderHost = parseExecutionHostId(
store.getWorktreeMeta(removalTarget.id)?.hostId ?? getRepoExecutionHostId(repo)
)
const folderSshPtyProvider =
folderHost?.kind === 'ssh' ? this.getSshProviderFn?.(folderHost.targetId) : undefined
const externalFolderHost = folderHost?.kind === 'ssh' || folderHost?.kind === 'runtime'
// This service runs inside the selected runtime, so runtime-stamped repos use its
// local PTY namespace; only a direct SSH connection is external from here.
const folderConnectionId = repo.connectionId?.trim() || null
const folderSshPtyProvider = folderConnectionId
? this.getSshProviderFn?.(folderConnectionId)
: undefined
const folderPtyProvider = folderSshPtyProvider ?? this.getLocalProvider()
if (folderPtyProvider) {
// Why: folder workspace deletion has no Git removal phase where PTYs
// would otherwise be swept; tear them down before hiding the workspace.
await killAllProcessesForWorktree(removalTarget.id, {
runtime: this,
// External host inventories must never sweep a same-id local workspace.
resolvedWorktreeId: removalTarget.id,
...(folderHost?.kind === 'ssh' ? { resolvedConnectionId: folderHost.targetId } : {}),
...(folderHost?.kind === 'runtime'
? { resolvedRuntimeEnvironmentId: folderHost.environmentId }
: {}),
...(folderConnectionId ? { resolvedConnectionId: folderConnectionId } : {}),
localProvider: folderPtyProvider,
onPtyStopped: this.onPtyStopped ?? undefined,
...(externalFolderHost
...(folderConnectionId
? {
includeProviderInventory:
folderHost?.kind === 'ssh' && Boolean(folderSshPtyProvider),
includeProviderInventory: Boolean(folderSshPtyProvider),
includeLocalRegistry: false
}
: {})
Expand Down
Loading