diff --git a/src/renderer/src/lib/local-path-open-guard.test.ts b/src/renderer/src/lib/local-path-open-guard.test.ts index 49b7ad5a617..b8dad86abdf 100644 --- a/src/renderer/src/lib/local-path-open-guard.test.ts +++ b/src/renderer/src/lib/local-path-open-guard.test.ts @@ -33,6 +33,8 @@ describe('getLocalPathOpenOwnerForRoute', () => { expect(getLocalPathOpenOwnerForRoute({})).toBe('local') expect(getLocalPathOpenOwnerForRoute({ runtimeEnvironmentId: ' env-2 ' })).toBe('runtime:env-2') expect(getLocalPathOpenOwnerForRoute({ connectionId: 'ssh-1' })).toBe('ssh:ssh-1') + expect(getLocalPathOpenOwnerForRoute({ connectionId: ' ssh-1 ' })).toBe('ssh:ssh-1') + expect(getLocalPathOpenOwnerForRoute({ connectionId: ' ' })).toBe('local') expect( getLocalPathOpenOwnerForRoute({ runtimeEnvironmentId: null, ownerUnresolved: true }) ).toBe('unresolved') diff --git a/src/renderer/src/lib/local-path-open-guard.ts b/src/renderer/src/lib/local-path-open-guard.ts index dfacad4046d..e4221eb1356 100644 --- a/src/renderer/src/lib/local-path-open-guard.ts +++ b/src/renderer/src/lib/local-path-open-guard.ts @@ -1,8 +1,8 @@ import { toast } from 'sonner' import { + getConnectionExecutionHostId, parseRoutableExecutionHostId, toRuntimeExecutionHostId, - toSshExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' import type { ExecutionHostOwnerMatch } from '../../../shared/worktree-execution-host-resolution' @@ -34,8 +34,7 @@ export function getLocalPathOpenOwnerForRoute(route: { if (runtimeEnvironmentId) { return toRuntimeExecutionHostId(runtimeEnvironmentId) } - const connectionId = route.connectionId?.trim() - return connectionId ? toSshExecutionHostId(connectionId) : 'local' + return getConnectionExecutionHostId(route.connectionId?.trim()) } /** The inverse of {@link getLocalPathOpenOwnerForRoute}, for callers that still take a route. */ diff --git a/src/renderer/src/lib/resolved-worktree-execution-host.test.ts b/src/renderer/src/lib/resolved-worktree-execution-host.test.ts index fb39bb3bc81..a155e4f94d0 100644 --- a/src/renderer/src/lib/resolved-worktree-execution-host.test.ts +++ b/src/renderer/src/lib/resolved-worktree-execution-host.test.ts @@ -47,6 +47,21 @@ describe('getResolvedExecutionHostIdForWorktree', () => { ) }) + it('falls back from the repo host stamp to its connection id, then local', () => { + const state = (repo: { connectionId: string | null; executionHostId?: ExecutionHostId }) => ({ + repos: [{ id: 'repo', ...repo }], + worktreesByRepo: { repo: [{ id: 'repo::wt', repoId: 'repo' }] } + }) + const resolve = (repo: { connectionId: string | null; executionHostId?: ExecutionHostId }) => + getResolvedExecutionHostIdForWorktree(state(repo), 'repo::wt') + expect(resolve({ connectionId: 'target-1', executionHostId: 'runtime:env-1' })).toBe( + 'runtime:env-1' + ) + expect(resolve({ connectionId: null, executionHostId: 'ssh:target-2' })).toBe('ssh:target-2') + expect(resolve({ connectionId: 'target-1' })).toBe('ssh:target-1') + expect(resolve({ connectionId: ' ' })).toBe('local') + }) + it('keeps missing folder catalogs unresolved but honors restored runtime ownership', () => { expect(getResolvedExecutionHostIdForWorktree({}, 'folder:missing')).toBeNull() expect( diff --git a/src/renderer/src/lib/resolved-worktree-execution-host.ts b/src/renderer/src/lib/resolved-worktree-execution-host.ts index a70e8eb7d57..24852e03be1 100644 --- a/src/renderer/src/lib/resolved-worktree-execution-host.ts +++ b/src/renderer/src/lib/resolved-worktree-execution-host.ts @@ -1,5 +1,6 @@ import { LOCAL_EXECUTION_HOST_ID, + getRepoExecutionHostId, parseExecutionHostId, toSshExecutionHostId, type ExecutionHostId @@ -91,13 +92,7 @@ export function getResolvedExecutionHostIdForWorktree( if (!repo) { return null } - const explicitRepoHost = parseExecutionHostId(repo.executionHostId) - if (explicitRepoHost) { - return explicitRepoHost.id - } - return repo.connectionId?.trim() - ? toSshExecutionHostId(repo.connectionId) - : LOCAL_EXECUTION_HOST_ID + return getRepoExecutionHostId(repo) } /** diff --git a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts index 2ac3eb20241..7f4c94ccf90 100644 --- a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts +++ b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.test.ts @@ -1,6 +1,23 @@ import { describe, expect, it } from 'vitest' +import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types' import type { DetectedWorktreeListResult } from '../../../../../../shared/worktree/types' -import { applyDetectedWorktreeUpdates } from './detected-worktree-meta' +import { applyDetectedWorktreeUpdates, folderWorkspaceMatchesHost } from './detected-worktree-meta' + +describe('folderWorkspaceMatchesHost', () => { + it('prefers the host stamp, then the connection id, then local', () => { + type Row = Pick + const ssh: Row = { executionHostId: 'ssh:target-1', connectionId: null } + expect(folderWorkspaceMatchesHost(ssh, 'ssh:target-1')).toBe(true) + expect(folderWorkspaceMatchesHost(ssh, 'local')).toBe(false) + const stamped: Row = { executionHostId: 'runtime:env-1', connectionId: 'target-1' } + expect(folderWorkspaceMatchesHost(stamped, 'runtime:env-1')).toBe(true) + expect(folderWorkspaceMatchesHost(stamped, 'ssh:target-1')).toBe(false) + const legacy: Row = { executionHostId: undefined, connectionId: 'target-1' } + expect(folderWorkspaceMatchesHost(legacy, 'ssh:target-1')).toBe(true) + expect(folderWorkspaceMatchesHost({ connectionId: ' ' }, 'local')).toBe(true) + expect(folderWorkspaceMatchesHost({ connectionId: null }, 'local')).toBe(true) + }) +}) describe('applyDetectedWorktreeUpdates display-name provenance', () => { it('projects pinning changes into detected rows', () => { diff --git a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts index 491b38df33c..cd57cf0464c 100644 --- a/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts +++ b/src/renderer/src/store/slices/worktrees/listing/detected-worktree-meta.ts @@ -6,8 +6,7 @@ import type { DetectedWorktreeListResult, Worktree } from '../../../../../../sha import type { ExecutionHostId } from '../../../../../../shared/execution-host' import { LOCAL_EXECUTION_HOST_ID, - parseExecutionHostId, - toSshExecutionHostId + getRepoExecutionHostId } from '../../../../../../shared/execution-host' import { parseWorkspaceKey } from '../../../../../../shared/workspace-scope' import { folderWorkspaceToWorktree } from '../../../../../../shared/folder-workspace-worktree' @@ -70,12 +69,7 @@ export function folderWorkspaceMatchesHost( workspace: Pick, executionHostId: ExecutionHostId ): boolean { - return ( - (parseExecutionHostId(workspace.executionHostId)?.id ?? - (workspace.connectionId?.trim() - ? toSshExecutionHostId(workspace.connectionId) - : LOCAL_EXECUTION_HOST_ID)) === executionHostId - ) + return getRepoExecutionHostId(workspace) === executionHostId } /** Stable synthetic row per folder workspace object, so selectors keep referential equality. */ diff --git a/src/shared/execution-host-registry.ts b/src/shared/execution-host-registry.ts index 8bdc713793f..c824b65160c 100644 --- a/src/shared/execution-host-registry.ts +++ b/src/shared/execution-host-registry.ts @@ -6,6 +6,7 @@ import { getSettingsFocusedExecutionHostId, getSshTargetIdForExecutionHost, isRuntimeOwnedSshTargetId, + normalizeHostPart, parseExecutionHostId, toRuntimeExecutionHostId, toSshExecutionHostId, @@ -55,11 +56,6 @@ type RuntimeStatusByEnvironmentId = ReadonlyMap - normalizeConnectionId( + normalizeHostPart( workspace.connectionId ?? state.projectGroups.find((entry) => entry.id === workspace.projectGroupId)?.connectionId )