fix(sidebar): preserve folder owner row identity

This commit is contained in:
Brennan Benson
2026-08-10 13:36:40 -07:00
parent 2e0c0c0332
commit d9d80e705f
6 changed files with 214 additions and 11 deletions
@@ -13,6 +13,7 @@ import type { RuntimeCompatVerdict } from '../../../../shared/protocol-compat'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import type { FolderWorkspace, ProjectGroup, Repo } from '../../../../shared/types'
import type { Row } from './worktree-list-groups'
import { getFolderWorkspaceExecutionHostIdForRows } from './worktree-list-host-filtering'
export type HostHeaderRow = {
type: 'host-header'
@@ -54,17 +55,16 @@ function getRepoHostId(
return defaultHostId
}
function getSshHostId(connectionId: string): ExecutionHostId {
return `ssh:${encodeURIComponent(connectionId)}` as ExecutionHostId
}
function getFolderWorkspaceHostId(
folderWorkspace: Pick<FolderWorkspace, 'connectionId'>,
projectGroup: Pick<ProjectGroup, 'connectionId'>,
folderWorkspace: Pick<FolderWorkspace, 'connectionId' | 'executionHostId'>,
projectGroup: Pick<ProjectGroup, 'connectionId' | 'executionHostId'>,
defaultHostId: ExecutionHostId
): ExecutionHostId {
const connectionId = folderWorkspace.connectionId ?? projectGroup.connectionId
return connectionId ? getSshHostId(connectionId) : defaultHostId
return getFolderWorkspaceExecutionHostIdForRows({
folderWorkspace,
projectGroup,
defaultHostId
})
}
function getRowHostId(row: Row, defaultHostId: ExecutionHostId): ExecutionHostId | null {
@@ -0,0 +1,124 @@
import { describe, expect, it } from 'vitest'
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
import { addHostSectionRows, type HostSectionRow } from './host-section-rows'
import type { Row } from './worktree-list-groups'
function rowKey(row: HostSectionRow): string {
return row.type === 'item' ? row.rowKey : row.key
}
describe('runtime folder host section rows', () => {
it('groups a runtime folder row under its stamped owner', () => {
const projectGroup: ProjectGroup = {
id: 'group-1',
name: 'Runtime folder',
parentPath: '/srv/project',
connectionId: null,
executionHostId: 'runtime:env-1',
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const folderWorkspace: FolderWorkspace = {
id: 'folder-1',
projectGroupId: projectGroup.id,
name: 'Folder workspace',
folderPath: '/srv/project',
connectionId: null,
executionHostId: 'runtime:env-1',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 1,
createdAt: 1,
updatedAt: 1
}
const localProjectGroup: ProjectGroup = {
...projectGroup,
id: 'local-group',
name: 'Local folder',
executionHostId: 'local'
}
const localFolderWorkspace: FolderWorkspace = {
...folderWorkspace,
id: 'local-folder',
projectGroupId: localProjectGroup.id,
name: 'Local workspace',
executionHostId: 'local'
}
const rows: Row[] = [
{
type: 'header',
key: 'project-group:local',
label: 'Local folder',
count: 1,
tone: 'text-foreground',
hostWorktreeCounts: new Map([['local', 1]]),
folderWorkspaceIds: [localFolderWorkspace.id]
},
{
type: 'folder-workspace',
key: 'folder-workspace:local-folder',
folderWorkspace: localFolderWorkspace,
projectGroup: localProjectGroup,
depth: 0,
groupDepth: 1
},
{
type: 'header',
key: 'project-group:runtime',
label: 'Runtime folder',
count: 1,
tone: 'text-foreground',
hostWorktreeCounts: new Map([['runtime:env-1', 1]]),
folderWorkspaceIds: [folderWorkspace.id]
},
{
type: 'folder-workspace',
key: 'folder-workspace:folder-1',
folderWorkspace,
projectGroup,
depth: 0,
groupDepth: 1
}
]
const sectioned = addHostSectionRows({
rows,
hostOptions: [
{
id: 'local',
kind: 'local',
label: 'Local Mac',
detail: 'This computer',
health: 'local'
},
{
id: 'runtime:env-1',
kind: 'runtime',
label: 'Runtime',
detail: 'Orca server',
health: 'available'
}
],
workspaceHostScope: 'all',
defaultHostId: 'local'
})
expect(sectioned.map(rowKey)).toEqual([
'host:local',
'project-group:local',
'folder-workspace:local-folder',
'host:runtime:env-1',
'project-group:runtime',
'folder-workspace:folder-1'
])
})
})
@@ -1,8 +1,10 @@
import { describe, expect, it } from 'vitest'
import type { ProjectGroup } from '../../../../shared/types'
import { getFolderWorkspaceRowKey } from '../../../../shared/folder-workspaces'
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
import {
buildProjectGroupSidebarIndex,
findProjectGroupForSidebarOwner,
getAmbiguousFolderWorkspaceSidebarIds,
getProjectGroupSidebarIdentity,
hasSingleProjectGroupMutationOwner
} from './project-group-sidebar-identity'
@@ -24,6 +26,30 @@ function group(id: string, executionHostId: string): ProjectGroup {
}
describe('project-group sidebar identity', () => {
it('keeps bare folder row keys until owner qualification is required', () => {
const workspace: FolderWorkspace = {
id: 'same-id',
projectGroupId: 'group-1',
name: 'Runtime folder',
folderPath: '/workspace',
executionHostId: 'runtime:env-1',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 1,
updatedAt: 1
}
expect(getFolderWorkspaceRowKey(workspace)).toBe('folder-workspace:same-id')
expect(getFolderWorkspaceRowKey(workspace, [], true)).toBe(
'folder-workspace:runtime%3Aenv-1:same-id'
)
})
it('resolves duplicate ids only with their catalog owner', () => {
const local = group('same-id', 'local')
const runtime = group('same-id', 'runtime:env-1')
@@ -35,6 +61,20 @@ describe('project-group sidebar identity', () => {
expect(getProjectGroupSidebarIdentity(local)).not.toBe(getProjectGroupSidebarIdentity(runtime))
})
it('marks folder row ids ambiguous only across owners', () => {
const local = group('local-group', 'local')
const runtime = group('runtime-group', 'runtime:env-1')
const index = buildProjectGroupSidebarIndex([local, runtime])
expect(
getAmbiguousFolderWorkspaceSidebarIds(index, [
{ id: 'same-id', projectGroupId: local.id, executionHostId: 'local' },
{ id: 'same-id', projectGroupId: runtime.id, executionHostId: 'runtime:env-1' },
{ id: 'unique-id', projectGroupId: runtime.id, executionHostId: 'runtime:env-1' }
])
).toEqual(new Set(['same-id']))
})
it('does not fall back to a foreign group for an explicit stale owner', () => {
const local = group('same-id', 'local')
const index = buildProjectGroupSidebarIndex([local])
@@ -137,6 +137,32 @@ export function findProjectGroupForFolderWorkspace(
return findProjectGroupForSidebarOwner(index, workspace.projectGroupId, ownerHostId)
}
export function getAmbiguousFolderWorkspaceSidebarIds(
index: ProjectGroupSidebarIndex,
workspaces: readonly Pick<
FolderWorkspace,
'id' | 'connectionId' | 'executionHostId' | 'projectGroupId'
>[]
): ReadonlySet<string> {
const ownerById = new Map<string, ExecutionHostId>()
const ambiguousIds = new Set<string>()
for (const workspace of workspaces) {
const group = findProjectGroupForFolderWorkspace(index, workspace)
if (!group || ambiguousIds.has(workspace.id)) {
continue
}
const ownerHostId = getProjectGroupOwnerHostId(group)
const existingOwnerHostId = ownerById.get(workspace.id)
if (existingOwnerHostId && existingOwnerHostId !== ownerHostId) {
ownerById.delete(workspace.id)
ambiguousIds.add(workspace.id)
} else {
ownerById.set(workspace.id, ownerHostId)
}
}
return ambiguousIds
}
export function findProjectGroupParentForSidebar(
index: ProjectGroupSidebarIndex,
group: ProjectGroup
@@ -61,6 +61,7 @@ import {
findProjectGroupForFolderWorkspace,
findProjectGroupForRepo,
findProjectGroupParentForSidebar,
getAmbiguousFolderWorkspaceSidebarIds,
getProjectGroupOwnerHostId,
getProjectGroupSidebarIdentity
} from './project-group-sidebar-identity'
@@ -1407,6 +1408,10 @@ export function buildRows(
}
const projectGroupIndex = buildProjectGroupSidebarIndex(projectGroups)
const ambiguousFolderWorkspaceIds = getAmbiguousFolderWorkspaceSidebarIds(
projectGroupIndex,
folderWorkspaces
)
const groupByProjectGroupIdentity = new Map<string | null, OrderedGroupEntry[]>()
for (const entry of orderedGroups) {
const repo = entry[1].repo
@@ -1585,7 +1590,11 @@ export function buildRows(
for (const folderWorkspace of folderWorkspacesByProjectGroupIdentity.get(identity) ?? []) {
result.push({
type: 'folder-workspace',
key: getFolderWorkspaceRowKey(folderWorkspace, [projectGroup]),
key: getFolderWorkspaceRowKey(
folderWorkspace,
[projectGroup],
ambiguousFolderWorkspaceIds.has(folderWorkspace.id)
),
folderWorkspace,
projectGroup,
depth: 0,
+5 -1
View File
@@ -68,8 +68,12 @@ export function getFolderWorkspaceIdentity(
export function getFolderWorkspaceRowKey(
workspace: Pick<FolderWorkspace, 'id' | 'connectionId' | 'executionHostId' | 'projectGroupId'>,
projectGroups: readonly ProjectGroup[] = []
projectGroups: readonly ProjectGroup[] = [],
qualifyOwner = false
): string {
if (!qualifyOwner) {
return `folder-workspace:${workspace.id}`
}
const ownerHostId = getFolderWorkspaceCatalogOwnerHostId(workspace, projectGroups)
return `folder-workspace:${encodeURIComponent(ownerHostId)}:${encodeURIComponent(workspace.id)}`
}