mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 08:04:05 +00:00
fix(sidebar): preserve folder owner row identity
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user