Fix worktree host ownership consistency

This commit is contained in:
Brennan Benson
2026-07-16 23:17:05 -07:00
parent 2efe272229
commit 82c6ee1303
8 changed files with 60 additions and 32 deletions
@@ -215,6 +215,7 @@ import {
ALL_EXECUTION_HOSTS_SCOPE,
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
getWorktreeExecutionHostId,
type ExecutionHostId
} from '../../../../shared/execution-host'
import { getRepoHeaderCreateState } from './repo-header-create-state'
@@ -2115,10 +2116,7 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
const targetWorktree = worktrees.find((w) => w.id === pendingRevealWorktree.worktreeId)
const targetRepo = targetWorktree ? repoMap.get(targetWorktree.repoId) : undefined
if (targetWorktree) {
const hostId =
targetRepo && (targetRepo.connectionId || targetRepo.executionHostId)
? getRepoExecutionHostId(targetRepo)
: defaultHostId
const hostId = getWorktreeExecutionHostId(targetWorktree, targetRepo, defaultHostId)
const hostGroupKey = `host:${hostId}`
if (collapsedGroups.has(hostGroupKey)) {
toggleGroup(hostGroupKey)
@@ -3,6 +3,7 @@ import {
LOCAL_EXECUTION_HOST_ID,
getLocalExecutionHostLabel,
getRepoExecutionHostId,
getWorktreeExecutionHostId,
type ExecutionHostId,
type ExecutionHostKind,
type ExecutionHostScope
@@ -69,9 +70,7 @@ function getFolderWorkspaceHostId(
function getRowHostId(row: Row, defaultHostId: ExecutionHostId): ExecutionHostId | null {
switch (row.type) {
case 'item':
// Why: runtime and SSH snapshots can carry a more precise owner than the
// repo fallback; header localization already uses this worktree identity.
return row.worktree.hostId ?? getRepoHostId(row.repo, defaultHostId)
return getWorktreeExecutionHostId(row.worktree, row.repo, defaultHostId)
case 'pending-creation':
case 'imported-worktrees-card':
case 'new-external-worktrees-inbox':
@@ -329,6 +329,25 @@ describe('computeVisibleWorktreeIds', () => {
expect(result).toEqual([local.id])
})
it('uses explicit worktree ownership when it differs from the repo host', () => {
const runtime = makeWorktree('runtime', 'repo1')
runtime.hostId = 'runtime:env-1'
const runtimeResult = computeVisibleWorktreeIds(
{ repo1: [runtime] },
[runtime.id],
visibleOptions({ workspaceHostScope: 'runtime:env-1' })
)
const localResult = computeVisibleWorktreeIds(
{ repo1: [runtime] },
[runtime.id],
visibleOptions({ workspaceHostScope: 'local' })
)
expect(runtimeResult).toEqual([runtime.id])
expect(localResult).toEqual([])
})
it('keeps every host visible when workspace host scope is all', () => {
const local = makeWorktree('local', 'repo1')
const remote = makeWorktree('remote', 'repo2')
@@ -6,8 +6,8 @@ import { getAllWorktreesFromState, getRepoMapFromState } from '@/store/selectors
import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants'
import {
ALL_EXECUTION_HOSTS_SCOPE,
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
getWorktreeExecutionHostId,
type ExecutionHostId,
type ExecutionHostScope
} from '../../../../shared/execution-host'
@@ -153,10 +153,7 @@ export function computeVisibleWorktreeIds(
if (!repo) {
return false
}
const hostId =
repo.connectionId || repo.executionHostId
? getRepoExecutionHostId(repo)
: opts.defaultHostId
const hostId = getWorktreeExecutionHostId(w, repo, opts.defaultHostId)
return visibleHostIdSet.has(hostId)
})
}
@@ -38,6 +38,7 @@ import {
getExecutionHostLabel,
LOCAL_EXECUTION_HOST_ID,
getRepoExecutionHostId,
getWorktreeExecutionHostId,
type ExecutionHostId
} from '../../../../shared/execution-host'
import { parseWslUncPath } from '../../../../shared/wsl-paths'
@@ -476,7 +477,7 @@ function emitPinnedGroup(
const pinnedRepoOrder: string[] = []
const seenPinnedRepoIds = new Set<string>()
for (const worktree of pinned) {
const hostId = getWorktreeHostId(worktree, repoMap, defaultHostId)
const hostId = getWorktreeExecutionHostId(worktree, repoMap.get(worktree.repoId), defaultHostId)
hostWorktreeCounts.set(hostId, (hostWorktreeCounts.get(hostId) ?? 0) + 1)
const hostIds = hostWorktreeIds.get(hostId) ?? []
hostIds.push(worktree.id)
@@ -730,20 +731,6 @@ function getMixedHostContextLabels(
return uniqueLabels.size > 1 ? labelsByRepoId : undefined
}
function getWorktreeHostId(
worktree: Worktree,
repoMap: Map<string, Repo>,
defaultHostId: ExecutionHostId
): ExecutionHostId {
if (worktree.hostId) {
return worktree.hostId
}
const repo = repoMap.get(worktree.repoId)
return repo && (repo.connectionId || repo.executionHostId)
? getRepoExecutionHostId(repo)
: defaultHostId
}
function getHostWorktreeCounts(
worktrees: readonly Worktree[],
repoMap: Map<string, Repo>,
@@ -759,7 +746,7 @@ function getHostWorktreeCounts(
continue
}
seenWorktreeIds.add(worktree.id)
const hostId = getWorktreeHostId(worktree, repoMap, defaultHostId)
const hostId = getWorktreeExecutionHostId(worktree, repoMap.get(worktree.repoId), defaultHostId)
counts.set(hostId, (counts.get(hostId) ?? 0) + 1)
}
return counts
@@ -780,7 +767,7 @@ function getHostWorktreeIds(
continue
}
seenWorktreeIds.add(worktree.id)
const hostId = getWorktreeHostId(worktree, repoMap, defaultHostId)
const hostId = getWorktreeExecutionHostId(worktree, repoMap.get(worktree.repoId), defaultHostId)
const ids = idsByHost.get(hostId) ?? []
ids.push(worktree.id)
idsByHost.set(hostId, ids)
@@ -1,4 +1,4 @@
import { getRepoExecutionHostId } from '../../../../shared/execution-host'
import { getWorktreeExecutionHostId } from '../../../../shared/execution-host'
import type { Repo, Worktree, WorktreeLineage } from '../../../../shared/types'
import { canAssignWorktreeParent } from './worktree-parent-eligibility'
@@ -15,7 +15,7 @@ function getWorktreeOwnerHostId(
repoMap: Map<string, Pick<Repo, 'connectionId' | 'executionHostId'>>
): string | null {
const repo = repoMap.get(worktree.repoId)
return worktree.hostId ?? (repo ? getRepoExecutionHostId(repo) : null)
return repo ? getWorktreeExecutionHostId(worktree, repo) : (worktree.hostId ?? null)
}
export function getEligibleWorktreeParents({
+15
View File
@@ -5,6 +5,7 @@ import {
getLocalExecutionHostLabel,
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
getWorktreeExecutionHostId,
normalizeExecutionHostOrder,
normalizeExecutionHostScope,
normalizeVisibleExecutionHostIds,
@@ -87,6 +88,20 @@ describe('execution host identity', () => {
expect(getRepoExecutionHostId({ connectionId: 'ssh-target-1' })).toBe('ssh:ssh-target-1')
})
it('prefers explicit worktree ownership before repo and focused-host fallbacks', () => {
expect(
getWorktreeExecutionHostId(
{ hostId: 'runtime:workspace-owner' },
{ connectionId: 'repo-owner' },
'runtime:focused-host'
)
).toBe('runtime:workspace-owner')
expect(
getWorktreeExecutionHostId({}, { connectionId: 'repo-owner' }, 'runtime:focused-host')
).toBe('ssh:repo-owner')
expect(getWorktreeExecutionHostId({}, {}, 'runtime:focused-host')).toBe('runtime:focused-host')
})
it('derives focused host compatibility from active runtime settings', () => {
expect(getSettingsFocusedExecutionHostId(null)).toBe(LOCAL_EXECUTION_HOST_ID)
expect(getSettingsFocusedExecutionHostId({ activeRuntimeEnvironmentId: 'runtime-1' })).toBe(
+14 -1
View File
@@ -1,4 +1,4 @@
import type { GlobalSettings, Repo } from './types'
import type { GlobalSettings, Repo, Worktree } from './types'
export const LOCAL_EXECUTION_HOST_ID = 'local'
export const ALL_EXECUTION_HOSTS_SCOPE = 'all'
@@ -150,6 +150,19 @@ export function getRepoExecutionHostId(
return connectionId ? toSshExecutionHostId(connectionId) : LOCAL_EXECUTION_HOST_ID
}
export function getWorktreeExecutionHostId(
worktree: Pick<Worktree, 'hostId'>,
repo: Pick<Repo, 'connectionId' | 'executionHostId'> | undefined,
defaultHostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID
): ExecutionHostId {
// Why: runtime and SSH snapshots can identify a more precise owner than
// the repo fallback; every sidebar host decision must use the same precedence.
return (
worktree.hostId ??
(repo?.connectionId || repo?.executionHostId ? getRepoExecutionHostId(repo) : defaultHostId)
)
}
export function getSettingsFocusedExecutionHostId(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
): ExecutionHostId {