From ec14a28181a00da1381dcf07c39da29d11c7f730 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 27 Jul 2026 19:17:40 -0700 Subject: [PATCH] fix(worktrees): stop terminals after external deletion --- ...g-worktree-terminal-reconciliation.test.ts | 100 ++++++++++++++++++ ...issing-worktree-terminal-reconciliation.ts | 79 ++++++++++++++ src/main/runtime/orca-runtime.test.ts | 100 ++++++++++++++++++ src/main/runtime/orca-runtime.ts | 52 ++++++++- ...worktree-missing-terminal-teardown.test.ts | 34 ++++++ .../runtime/rpc/methods/worktree-schemas.ts | 5 + src/main/runtime/rpc/methods/worktree.ts | 13 ++- .../src/store/slices/worktrees.test.ts | 68 ++++++++++++ src/renderer/src/store/slices/worktrees.ts | 65 +++++++++++- 9 files changed, 511 insertions(+), 5 deletions(-) create mode 100644 src/main/runtime/missing-worktree-terminal-reconciliation.test.ts create mode 100644 src/main/runtime/missing-worktree-terminal-reconciliation.ts create mode 100644 src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts new file mode 100644 index 00000000000..968f9b170c0 --- /dev/null +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it, vi } from 'vitest' +import type { IPtyProvider } from '../providers/types' +import type { Repo } from '../../shared/types' +import type { OrcaRuntimeService } from './orca-runtime' +import { stopMissingWorktreeTerminals } from './missing-worktree-terminal-reconciliation' + +function createProvider(sessionIds: string[]): IPtyProvider { + return { + listProcesses: vi.fn(async () => + sessionIds.map((id) => ({ id, cwd: '/workspace', title: 'shell' })) + ), + shutdown: vi.fn(async () => {}) + } as unknown as IPtyProvider +} + +function createRuntime(): OrcaRuntimeService { + return { + stopTerminalsForWorktree: vi.fn(async () => ({ stopped: 0 })) + } as unknown as OrcaRuntimeService +} + +const localRepo: Repo = { + id: 'repo-1', + path: '/workspace/repo', + displayName: 'Repo', + badgeColor: '#000', + addedAt: 1 +} + +describe('stopMissingWorktreeTerminals', () => { + it('stops only locally owned worktrees absent from the authoritative scan', async () => { + const deletedId = 'repo-1::/workspace/deleted' + const survivingId = 'repo-1::/workspace/surviving' + const provider = createProvider([ + `${deletedId}@@deleted-session`, + `${survivingId}@@surviving-session` + ]) + const runtime = createRuntime() + + const result = await stopMissingWorktreeTerminals( + localRepo, + [deletedId, survivingId, 'repo-2::/workspace/other'], + [survivingId], + { + runtime, + getLocalProvider: () => provider, + getSshProvider: () => undefined + } + ) + + expect(result).toEqual({ stoppedWorktreeIds: [deletedId] }) + expect(provider.shutdown).toHaveBeenCalledWith( + `${deletedId}@@deleted-session`, + expect.objectContaining({ immediate: true }) + ) + expect(provider.shutdown).not.toHaveBeenCalledWith( + `${survivingId}@@surviving-session`, + expect.anything() + ) + }) + + it('uses the owning SSH provider without consulting the local provider', async () => { + const deletedId = 'repo-1::/workspace/deleted' + const localProvider = createProvider([`${deletedId}@@local-session`]) + const sshProvider = createProvider([`${deletedId}@@ssh-session`]) + const getSshProvider = vi.fn(() => sshProvider) + + await stopMissingWorktreeTerminals({ ...localRepo, connectionId: 'ssh-1' }, [deletedId], [], { + runtime: createRuntime(), + getLocalProvider: () => localProvider, + getSshProvider + }) + + expect(getSshProvider).toHaveBeenCalledWith('ssh-1') + expect(sshProvider.shutdown).toHaveBeenCalledWith( + `${deletedId}@@ssh-session`, + expect.objectContaining({ immediate: true }) + ) + expect(localProvider.listProcesses).not.toHaveBeenCalled() + }) + + it('still stops graph-visible sessions when the owning provider is unavailable', async () => { + const deletedId = 'repo-1::/workspace/deleted' + const runtime = createRuntime() + + const result = await stopMissingWorktreeTerminals( + { ...localRepo, connectionId: 'ssh-1' }, + [deletedId], + [], + { + runtime, + getLocalProvider: () => null, + getSshProvider: () => undefined + } + ) + + expect(result).toEqual({ stoppedWorktreeIds: [deletedId] }) + expect(runtime.stopTerminalsForWorktree).toHaveBeenCalledWith(deletedId) + }) +}) diff --git a/src/main/runtime/missing-worktree-terminal-reconciliation.ts b/src/main/runtime/missing-worktree-terminal-reconciliation.ts new file mode 100644 index 00000000000..e7fa2b5bbd8 --- /dev/null +++ b/src/main/runtime/missing-worktree-terminal-reconciliation.ts @@ -0,0 +1,79 @@ +import type { IPtyProvider } from '../providers/types' +import type { Repo } from '../../shared/types' +import { splitWorktreeId } from '../../shared/worktree-id' +import { mapWithConcurrency } from '../../shared/map-with-concurrency' +import type { OrcaRuntimeService } from './orca-runtime' +import { killAllProcessesForWorktree } from './worktree-teardown' + +const MISSING_WORKTREE_TEARDOWN_CONCURRENCY = 4 + +type MissingWorktreeTerminalReconciliationDeps = { + runtime: OrcaRuntimeService + getLocalProvider: () => IPtyProvider | null + getSshProvider: (connectionId: string) => IPtyProvider | undefined + onPtyStopped?: (ptyId: string) => void +} + +export async function stopMissingWorktreeTerminals( + repo: Repo, + knownWorktreeIds: readonly string[], + detectedWorktreeIds: readonly string[], + deps: MissingWorktreeTerminalReconciliationDeps +): Promise<{ stoppedWorktreeIds: string[] }> { + const detectedIds = new Set(detectedWorktreeIds) + const missingIds = [ + ...new Set( + knownWorktreeIds.filter( + (worktreeId) => + splitWorktreeId(worktreeId)?.repoId === repo.id && !detectedIds.has(worktreeId) + ) + ) + ] + if (missingIds.length === 0) { + return { stoppedWorktreeIds: [] } + } + + const provider = repo.connectionId + ? deps.getSshProvider(repo.connectionId) + : deps.getLocalProvider() + if (!provider) { + const stoppedWorktreeIds = ( + await mapWithConcurrency( + missingIds, + MISSING_WORKTREE_TEARDOWN_CONCURRENCY, + async (worktreeId) => { + try { + await deps.runtime.stopTerminalsForWorktree(worktreeId) + return worktreeId + } catch { + return null + } + } + ) + ).filter((worktreeId): worktreeId is string => worktreeId !== null) + return { stoppedWorktreeIds } + } + + const stoppedWorktreeIds = ( + await mapWithConcurrency( + missingIds, + MISSING_WORKTREE_TEARDOWN_CONCURRENCY, + async (worktreeId) => { + try { + await killAllProcessesForWorktree(worktreeId, { + runtime: deps.runtime, + localProvider: provider, + onPtyStopped: deps.onPtyStopped, + ...(repo.connectionId ? { includeLocalRegistry: false } : {}) + }) + return worktreeId + } catch (error) { + console.warn(`[worktree-teardown] Failed to stop missing workspace ${worktreeId}`, error) + return null + } + } + ) + ).filter((worktreeId): worktreeId is string => worktreeId !== null) + + return { stoppedWorktreeIds } +} diff --git a/src/main/runtime/orca-runtime.test.ts b/src/main/runtime/orca-runtime.test.ts index e61b64df170..ef5a340e02e 100644 --- a/src/main/runtime/orca-runtime.test.ts +++ b/src/main/runtime/orca-runtime.test.ts @@ -31373,6 +31373,106 @@ describe('OrcaRuntimeService', () => { expect(removeWorktreeLineage).not.toHaveBeenCalled() }) + it('revalidates missing worktrees before stopping their local provider sessions', async () => { + const deletedId = `${TEST_REPO_ID}::/tmp/deleted` + const survivingId = `${TEST_REPO_ID}::/tmp/surviving` + const localProvider = { + listProcesses: vi.fn(async () => [ + { id: `${deletedId}@@deleted-session`, cwd: '/tmp/deleted', title: 'shell' }, + { id: `${survivingId}@@surviving-session`, cwd: '/tmp/surviving', title: 'shell' } + ]), + shutdown: vi.fn(async () => {}) + } + const runtime = new OrcaRuntimeService(store, undefined, { + getLocalProvider: () => localProvider as never + }) + vi.spyOn(runtime, 'listDetectedManagedWorktrees').mockResolvedValue({ + repoId: TEST_REPO_ID, + authoritative: true, + source: 'git', + worktrees: [{ id: survivingId }] as never + }) + + const result = await runtime.teardownMissingManagedWorktreeTerminals(`id:${TEST_REPO_ID}`, [ + deletedId, + survivingId + ]) + + expect(result).toEqual({ stoppedWorktreeIds: [deletedId] }) + expect(localProvider.shutdown).toHaveBeenCalledWith( + `${deletedId}@@deleted-session`, + expect.objectContaining({ immediate: true }) + ) + expect(localProvider.shutdown).not.toHaveBeenCalledWith( + `${survivingId}@@surviving-session`, + expect.anything() + ) + }) + + it('does not stop sessions after a non-authoritative revalidation', async () => { + const localProvider = { + listProcesses: vi.fn(async () => []), + shutdown: vi.fn(async () => {}) + } + const runtime = new OrcaRuntimeService(store, undefined, { + getLocalProvider: () => localProvider as never + }) + vi.spyOn(runtime, 'listDetectedManagedWorktrees').mockResolvedValue({ + repoId: TEST_REPO_ID, + authoritative: false, + source: 'metadata-fallback', + worktrees: [] + }) + + await runtime.teardownMissingManagedWorktreeTerminals(`id:${TEST_REPO_ID}`, [ + `${TEST_REPO_ID}::/tmp/deleted` + ]) + + expect(localProvider.listProcesses).not.toHaveBeenCalled() + }) + + it('uses the connection-scoped repo when local and SSH repo ids collide', async () => { + const deletedId = `${TEST_REPO_ID}::/tmp/deleted` + const localProvider = { + listProcesses: vi.fn(async () => []), + shutdown: vi.fn(async () => {}) + } + const sshProvider = { + listProcesses: vi.fn(async () => [ + { id: `${deletedId}@@ssh-session`, cwd: '/tmp/deleted', title: 'shell' } + ]), + shutdown: vi.fn(async () => {}) + } + const localRepo = store.getRepos()[0] + const sshRepo = { ...localRepo, connectionId: 'ssh-1' } + const runtime = new OrcaRuntimeService( + { + ...store, + getRepos: () => [localRepo, sshRepo] + } as never, + undefined, + { + getLocalProvider: () => localProvider as never, + getSshProvider: (connectionId) => + connectionId === 'ssh-1' ? (sshProvider as never) : undefined + } + ) + vi.spyOn(runtime, 'listDetectedManagedWorktrees').mockResolvedValue({ + repoId: TEST_REPO_ID, + authoritative: true, + source: 'git', + worktrees: [] + }) + + await runtime.teardownMissingManagedWorktreeTerminals(TEST_REPO_ID, [deletedId], 'ssh-1') + + expect(sshProvider.shutdown).toHaveBeenCalledWith( + `${deletedId}@@ssh-session`, + expect.objectContaining({ immediate: true }) + ) + expect(localProvider.listProcesses).not.toHaveBeenCalled() + }) + it('hydrates runtime detected lists with instance-validated legacy lineage', async () => { const parentPath = join(tmpdir(), 'worktree-parent') const childPath = join(tmpdir(), 'worktree-child') diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index f06a01d20f9..92a4c3ee7f3 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -885,6 +885,7 @@ import { registerTerminalViewAttributesApplier } from './terminal-view-attribute-store' import { killAllProcessesForWorktree, teardownRpcDeadline } from './worktree-teardown' +import { stopMissingWorktreeTerminals } from './missing-worktree-terminal-reconciliation' import { MobileNotificationReplayBuffer, type ReplayableMobileNotification @@ -17944,8 +17945,11 @@ export class OrcaRuntimeService { } } - async listDetectedManagedWorktrees(repoSelector: string): Promise { - const repo = await this.resolveRepoSelector(repoSelector) + async listDetectedManagedWorktrees( + repoSelector: string, + connectionId?: string | null + ): Promise { + const repo = await this.resolveRepoSelectorForConnection(repoSelector, connectionId) const store = this.requireStore() if (isFolderRepo(repo)) { const worktrees = listRuntimeFolderWorkspaces(store, repo) @@ -17995,6 +17999,50 @@ export class OrcaRuntimeService { } } + async teardownMissingManagedWorktreeTerminals( + repoSelector: string, + knownWorktreeIds: readonly string[], + connectionId?: string | null + ): Promise<{ stoppedWorktreeIds: string[] }> { + const repo = await this.resolveRepoSelectorForConnection(repoSelector, connectionId) + const detected = await this.listDetectedManagedWorktrees(`id:${repo.id}`, connectionId) + if (!detected.authoritative) { + return { stoppedWorktreeIds: [] } + } + return stopMissingWorktreeTerminals( + repo, + knownWorktreeIds, + detected.worktrees.map((worktree) => worktree.id), + { + runtime: this, + getLocalProvider: () => this.getLocalProvider(), + getSshProvider: (connectionId) => this.getSshProviderFn?.(connectionId), + onPtyStopped: this.onPtyStopped ?? undefined + } + ) + } + + private resolveRepoSelectorForConnection( + repoSelector: string, + connectionId?: string | null + ): Promise { + if (connectionId === undefined) { + return this.resolveRepoSelector(repoSelector) + } + const repoId = repoSelector.startsWith('id:') ? repoSelector.slice(3) : repoSelector + const matches = this.requireStore() + .getRepos() + .filter( + (repo) => + repo.id === repoId && + (repo.connectionId?.trim() || null) === (connectionId?.trim() || null) + ) + if (matches.length !== 1) { + throw new Error(matches.length > 1 ? 'selector_ambiguous' : 'repo_not_found') + } + return Promise.resolve(matches[0]) + } + private isRuntimeWorktreeVisible( worktree: Worktree, agentScratchWorktreePathMatcher?: AgentScratchWorktreePathMatcher diff --git a/src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts b/src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts new file mode 100644 index 00000000000..9ea904af68d --- /dev/null +++ b/src/main/runtime/rpc/methods/worktree-missing-terminal-teardown.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, vi } from 'vitest' +import type { OrcaRuntimeService } from '../../orca-runtime' +import { RpcDispatcher } from '../dispatcher' +import { WORKTREE_METHODS } from './worktree' + +describe('worktree missing-terminal teardown RPC', () => { + it('routes the connection-scoped request through the runtime owner', async () => { + const runtime = { + getRuntimeId: () => 'test-runtime', + teardownMissingManagedWorktreeTerminals: vi + .fn() + .mockResolvedValue({ stoppedWorktreeIds: ['repo-1::/workspace/deleted'] }) + } as unknown as OrcaRuntimeService + const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS }) + + const response = await dispatcher.dispatch({ + id: 'request-1', + authToken: 'token', + method: 'worktree.teardownMissingTerminals', + params: { + repo: 'repo-1', + worktreeIds: ['repo-1::/workspace/deleted'], + connectionId: 'ssh-1' + } + }) + + expect(runtime.teardownMissingManagedWorktreeTerminals).toHaveBeenCalledWith( + 'repo-1', + ['repo-1::/workspace/deleted'], + 'ssh-1' + ) + expect(response).toMatchObject({ ok: true }) + }) +}) diff --git a/src/main/runtime/rpc/methods/worktree-schemas.ts b/src/main/runtime/rpc/methods/worktree-schemas.ts index b79fb320f59..87843e27cc4 100644 --- a/src/main/runtime/rpc/methods/worktree-schemas.ts +++ b/src/main/runtime/rpc/methods/worktree-schemas.ts @@ -48,6 +48,11 @@ export const WorktreeDetectedListParams = z.object({ .pipe(z.string().min(1, 'Missing repo selector')) }) +export const WorktreeTeardownMissingTerminalsParams = WorktreeDetectedListParams.extend({ + worktreeIds: z.array(z.string().min(1)).max(10_000), + connectionId: z.string().nullable().optional() +}) + export const WorktreePsParams = z.object({ limit: OptionalFiniteNumber }) diff --git a/src/main/runtime/rpc/methods/worktree.ts b/src/main/runtime/rpc/methods/worktree.ts index 0f23bccac5e..b8333926343 100644 --- a/src/main/runtime/rpc/methods/worktree.ts +++ b/src/main/runtime/rpc/methods/worktree.ts @@ -19,7 +19,8 @@ import { WorktreeResolvePrBase, WorktreeSelector, WorktreeSet, - WorktreeSortOrder + WorktreeSortOrder, + WorktreeTeardownMissingTerminalsParams } from './worktree-schemas' export const WORKTREE_METHODS: RpcMethod[] = [ @@ -38,6 +39,16 @@ export const WORKTREE_METHODS: RpcMethod[] = [ params: WorktreeDetectedListParams, handler: async (params, { runtime }) => runtime.listDetectedManagedWorktrees(params.repo) }), + defineMethod({ + name: 'worktree.teardownMissingTerminals', + params: WorktreeTeardownMissingTerminalsParams, + handler: async (params, { runtime }) => + runtime.teardownMissingManagedWorktreeTerminals( + params.repo, + params.worktreeIds, + params.connectionId + ) + }), defineMethod({ name: 'worktree.lineageList', params: null, diff --git a/src/renderer/src/store/slices/worktrees.test.ts b/src/renderer/src/store/slices/worktrees.test.ts index bc8da63bb42..ae8bf057aa8 100644 --- a/src/renderer/src/store/slices/worktrees.test.ts +++ b/src/renderer/src/store/slices/worktrees.test.ts @@ -83,6 +83,13 @@ const mockApi = { runtimeEnvironments: { call: runtimeEnvironmentTransportCall }, + runtime: { + call: vi.fn().mockResolvedValue({ + id: 'runtime-call', + ok: true, + result: { stoppedWorktreeIds: [] } + }) + }, ephemeralVm: { cancelProvision: vi.fn().mockResolvedValue({ cancelled: true }), cleanup: vi.fn().mockResolvedValue({}), @@ -1038,6 +1045,66 @@ describe('fetchWorktrees', () => { expect(hasDismissedHugeRepoWarning(beginHugeRepoWarningProbe(hidden))).toBe(false) }) + it('awaits missing-worktree terminal teardown before purging renderer state', async () => { + const store = createTestStore() + const deleted = makeWorktree({ + id: 'repo1::/path/deleted', + repoId: 'repo1', + path: '/path/deleted' + }) + const surviving = makeWorktree({ + id: 'repo1::/path/surviving', + repoId: 'repo1', + path: '/path/surviving' + }) + let finishTeardown!: () => void + mockApi.runtime.call.mockImplementationOnce( + () => + new Promise((resolve) => { + finishTeardown = () => + resolve({ + id: 'teardown', + ok: true, + result: { stoppedWorktreeIds: [deleted.id] } + }) + }) + ) + mockApi.worktrees.listDetected.mockResolvedValueOnce(makeDetectedResult('repo1', [surviving])) + store.setState({ + repos: [ + { + id: 'repo1', + path: '/path/repo1', + displayName: 'Repo 1', + badgeColor: '#000', + addedAt: 0, + connectionId: 'ssh-1' + } + ], + worktreesByRepo: { repo1: [deleted, surviving] }, + detectedWorktreesByRepo: { + repo1: makeDetectedResult('repo1', [deleted, surviving]) + }, + tabsByWorktree: { + [deleted.id]: [{ id: 'tab-deleted', worktreeId: deleted.id }] + } + } as unknown as Partial) + + const refresh = store.getState().fetchWorktrees('repo1') + await vi.waitFor(() => expect(mockApi.runtime.call).toHaveBeenCalledTimes(1)) + + expect(mockApi.runtime.call).toHaveBeenCalledWith({ + method: 'worktree.teardownMissingTerminals', + params: { repo: 'repo1', worktreeIds: [deleted.id], connectionId: 'ssh-1' } + }) + expect(store.getState().tabsByWorktree[deleted.id]).toBeDefined() + + finishTeardown() + await refresh + + expect(store.getState().tabsByWorktree[deleted.id]).toBeUndefined() + }) + it('clears a hidden dismissal across hydrated fetch-all delete and recreation', async () => { const store = createTestStore() const visible = makeWorktree({ @@ -1174,6 +1241,7 @@ describe('fetchWorktrees', () => { expect(store.getState().worktreesByRepo.repo1).toEqual([fallback]) expect(store.getState().sortEpoch).toBe(8) expect(result).toBe(false) + expect(mockApi.runtime.call).not.toHaveBeenCalled() }) it('does not purge remembered right sidebar tabs on a transient empty refresh', async () => { diff --git a/src/renderer/src/store/slices/worktrees.ts b/src/renderer/src/store/slices/worktrees.ts index 02711db36f0..9276b19ab98 100644 --- a/src/renderer/src/store/slices/worktrees.ts +++ b/src/renderer/src/store/slices/worktrees.ts @@ -633,6 +633,35 @@ function isRuntimeMethodNotFoundError(error: unknown): boolean { return error instanceof RuntimeRpcCallError && error.code === 'method_not_found' } +async function teardownMissingWorktreeTerminalsBestEffort( + settings: AppState['settings'], + repoId: string, + connectionId: string | null | undefined, + knownWorktreeIds: readonly string[], + detected: DetectedWorktreeListResult +): Promise { + if (!detected.authoritative || knownWorktreeIds.length === 0) { + return + } + const detectedIds = new Set(detected.worktrees.map((worktree) => worktree.id)) + const missingIds = knownWorktreeIds.filter((worktreeId) => !detectedIds.has(worktreeId)) + if (missingIds.length === 0) { + return + } + try { + await callRuntimeRpc( + getActiveRuntimeTarget(settings), + 'worktree.teardownMissingTerminals', + { repo: repoId, worktreeIds: missingIds, connectionId: connectionId ?? null }, + { timeoutMs: 30_000 } + ) + } catch (error) { + if (!isRuntimeMethodNotFoundError(error)) { + console.warn(`Failed to stop terminals for missing worktrees in repo ${repoId}:`, error) + } + } +} + // Why: a mobile-scope web pairing is denied worktree/repo RPCs (else silently empty workspaces); surface one deduped toast (stable id) instead of spamming per-repo. const RUNTIME_SCOPE_FORBIDDEN_TOAST_ID = 'runtime-scope-forbidden' @@ -1003,6 +1032,8 @@ function detectedWorktreeRefreshKey( repoId: string, options: { executionHostId: ExecutionHostId + connectionId?: string | null + knownWorktreeIds: readonly string[] requireAuthoritative?: boolean reuseRecentCompatibilityFailure?: boolean } @@ -1029,6 +1060,8 @@ async function listDetectedWorktreesForRepoCoalesced( repoId: string, options: { executionHostId: ExecutionHostId + connectionId?: string | null + knownWorktreeIds: readonly string[] requireAuthoritative?: boolean reuseRecentCompatibilityFailure?: boolean } @@ -1060,6 +1093,13 @@ async function listDetectedWorktreesForRepoCoalesced( ) { throw new Error('runtime_environment_generation_changed') } + await teardownMissingWorktreeTerminalsBestEffort( + settings, + repoId, + options.connectionId, + options.knownWorktreeIds, + result + ) return result } finally { if (detectedWorktreeRefreshesInFlight.get(key) === refresh) { @@ -2373,10 +2413,18 @@ export const createWorktreeSlice: StateCreator const hostId = repoHostId(ownerState, repoId) const ownerWasMissingAtStart = !ownerState.repos.some((repo) => repo.id === repoId) const setup = getProjectHostSetupForRepoHost(ownerState, repoId, hostId) + const repoOwner = findRepoForHost(ownerState.repos, repoId, { + hostId, + settings: ownerState.settings + }) const result = await listDetectedWorktreesForRepoCoalesced( settingsForRepoOwner(ownerState, repoId, hostId), repoId, - { executionHostId: hostId } + { + executionHostId: hostId, + connectionId: repoOwner?.connectionId, + knownWorktreeIds: getKnownWorktreeIdsForPurge(ownerState, repoId, hostId) + } ) set((s) => { if (!repoHasExecutionHost(s, repoId, hostId, ownerWasMissingAtStart)) { @@ -2421,6 +2469,10 @@ export const createWorktreeSlice: StateCreator : repoHostId(ownerState, repoId, options?.executionHostId) const ownerWasMissingAtStart = repoOwners.length === 0 const setup = getProjectHostSetupForRepoHost(ownerState, repoId, hostId) + const repoOwner = findRepoForHost(ownerState.repos, repoId, { + hostId, + settings: ownerState.settings + }) const ownerSettings = settingsForRepoOwner(ownerState, repoId, hostId) const settings = useLocalOwner && ownerSettings?.activeRuntimeEnvironmentId @@ -2428,6 +2480,8 @@ export const createWorktreeSlice: StateCreator : ownerSettings const detected = await listDetectedWorktreesForRepoCoalesced(settings, repoId, { executionHostId: hostId, + connectionId: repoOwner?.connectionId, + knownWorktreeIds: getKnownWorktreeIdsForPurge(ownerState, repoId, hostId), requireAuthoritative: options?.requireAuthoritative }) if (options?.requireAuthoritative && !detected.authoritative) { @@ -2578,6 +2632,8 @@ export const createWorktreeSlice: StateCreator const settings = settingsForKnownRepoOwner(requestStartedState.settings, r) const detected = await listDetectedWorktreesForRepoCoalesced(settings, r.id, { executionHostId: hostId, + connectionId: r.connectionId, + knownWorktreeIds: getKnownWorktreeIdsForPurge(requestStartedState, r.id, hostId), reuseRecentCompatibilityFailure: true }) let incoming = toVisibleWorktrees(detected, hostId, setup) @@ -2663,7 +2719,12 @@ export const createWorktreeSlice: StateCreator const detected = await listDetectedWorktreesForRepoCoalesced( settingsForKnownRepoOwner(requestStartedState.settings, r), r.id, - { executionHostId: hostId, reuseRecentCompatibilityFailure: true } + { + executionHostId: hostId, + connectionId: r.connectionId, + knownWorktreeIds: getKnownWorktreeIdsForPurge(requestStartedState, r.id, hostId), + reuseRecentCompatibilityFailure: true + } ) let incoming = toVisibleWorktrees(detected, hostId, setup) const latestState = get()