fix: align group workspace owners and create result consumers

This commit is contained in:
Brennan Benson
2026-11-12 14:44:25 -08:00
parent aeba0a59c5
commit 0ee7280ec9
16 changed files with 55 additions and 21 deletions
@@ -1,3 +1,4 @@
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
import type { ProjectGroup } from '../../shared/project-group-types'
import type { Repo } from '../../shared/repo-types'
import type { FolderWorkspace } from '../../shared/folder-workspaces/folder-workspace-types'
@@ -38,10 +39,11 @@ export function createFolderWorkspaceConnectionResolver(
children.set(group.parentGroupId, siblings)
}
for (const repo of repos) {
const connectionId = getRepoRoutingConnectionId(repo)
const summary: Connections = {
count: 1,
local: !repo.connectionId,
first: repo.connectionId || undefined,
local: !connectionId,
first: connectionId ?? undefined,
multiple: false
}
if (repo.projectGroupId) {
@@ -1,3 +1,4 @@
import { createFolderWorkspaceConnectionResolver } from './folder-workspace-connection-index'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
@@ -41,6 +42,32 @@ function makeRepo(overrides: Partial<Repo> = {}): Repo {
}
describe('folder workspace path status', () => {
it.each([undefined, 'stale-target'])(
'routes indexed group teardown to the canonical SSH owner with legacy target %s',
(connectionId) => {
const workspace: FolderWorkspace = {
id: 'saved',
projectGroupId: 'group-1',
name: 'Saved',
folderPath: '/workspace/task',
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 0,
updatedAt: 0
}
const resolve = createFolderWorkspaceConnectionResolver(
[makeGroup()],
[makeRepo({ executionHostId: 'ssh:prod', connectionId })]
)
expect(resolve(workspace)).toEqual({ kind: 'ssh', connectionId: 'prod' })
}
)
it.each([false, true])(
'reports canonical owner ambiguity through the persisted folder status route (%s)',
async (reverse) => {
@@ -1,3 +1,4 @@
import { selectFolderWorkspaces } from '../workspaces/catalog/workspace-record-fields'
import { checkRuntimeGroupHooks } from '@/runtime/repo-hooks/runtime-hooks-client'
import { confirmInspectedArchiveHook } from '@/runtime/repo-hooks/ensure-hooks-confirmed'
import { projectGroupHookOwner } from '../../../../shared/workspace-hook-owner'
@@ -103,7 +104,7 @@ export function createFolderWorkspaceDeleteAction(
return false
}
set((s) => ({
folderWorkspaces: s.folderWorkspaces.filter(
folderWorkspaces: selectFolderWorkspaces(s).filter(
(workspace) =>
workspace.id !== folderWorkspaceId ||
getFolderWorkspaceHostId(workspace, s.projectGroups) !== ownerHostId
@@ -116,7 +117,7 @@ export function createFolderWorkspaceDeleteAction(
(owner, scope) =>
owner.workspaceId === workspaceKey && launchDraftSource(scope)?.origin === origin
)
if (!get().folderWorkspaces.some((workspace) => workspace.id === folderWorkspaceId)) {
if (!selectFolderWorkspaces(get()).some((workspace) => workspace.id === folderWorkspaceId)) {
// Folder/worktree browser registry keys coincide; retire Chromium guests before renderer cleanup.
await Promise.resolve(get().shutdownWorktreeBrowsers(workspaceKey))
// Why here too: a server-hosted folder's local pages use this desktop's profile, which the server can't reach.
@@ -157,7 +158,7 @@ export function createFolderWorkspaceDeleteAction(
worktreeId: workspaceKey,
executionHostId: ownerHostId,
worktreeName:
state.folderWorkspaces.find(
selectFolderWorkspaces(state).find(
(workspace) =>
workspace.id === folderWorkspaceId &&
getFolderWorkspaceHostId(workspace, state.projectGroups) === ownerHostId
@@ -114,12 +114,12 @@ export function createProjectGroupMutationActions(
))
const folders =
supported && ownerHostId && target.kind === 'environment'
? (
? selectFolderWorkspaces(
await callRuntimeRpc<{ folderWorkspaces: FolderWorkspace[] }>(
target,
'folderWorkspace.list'
)
).folderWorkspaces.map((row) =>
).map((row) =>
adoptFromEndpoint(target, {
kind: 'folderWorkspace',
row,
@@ -47,7 +47,7 @@ for (const selection of ['workspace', 'tab'] as const) {
if (!target || !other) {
throw new Error('Could not create folder workspaces')
}
return { target: target.id, other: other.id }
return { target: target.folderWorkspace.id, other: other.folderWorkspace.id }
}, folderPath)
const openPalette = async (): Promise<void> => {
// Native menu accelerators need desktop focus; drive the same palette through CDP.
+1 -1
View File
@@ -164,7 +164,7 @@ async function createHostFolderWorkspace(page: Page, folderPath: string): Promis
name: 'Paired skill folder E2E',
folderPath: path
})
return workspace.id
return workspace.folderWorkspace.id
}, folderPath)
}
+1 -1
View File
@@ -141,7 +141,7 @@ test.describe('PDF in a folder workspace', () => {
if (!folder) {
throw new Error('Missing fixture folder')
}
const worktreeId = `folder:${folder.id}`
const worktreeId = `folder:${folder.folderWorkspace.id}`
state.setActiveWorktree(worktreeId)
state.openFile({
filePath,
@@ -35,7 +35,7 @@ test('Resource Manager ends a tab-less terminal this run spawned', async ({
cols: 80,
rows: 24,
cwd: folderPath,
worktreeId: `folder:${folder.id}`,
worktreeId: `folder:${folder.folderWorkspace.id}`,
initiallyHidden: true
})
return spawned.id
@@ -43,7 +43,7 @@ test('names a folder terminal recovered from the daemon after restart without an
cols: 80,
rows: 24,
cwd: folderPath,
worktreeId: `folder:${folder.id}`,
worktreeId: `folder:${folder.folderWorkspace.id}`,
initiallyHidden: true
})
}, root)
@@ -37,13 +37,15 @@ test('cycles visible folder workspaces with shortcuts and focused list arrows',
if (!folder || !repo || !first || !last) {
throw new Error('Expected two git worktrees and one folder workspace')
}
await store.getState().updateFolderWorkspace(folder.id, { workspaceStatus: 'in-progress' })
await store
.getState()
.updateFolderWorkspace(folder.folderWorkspace.id, { workspaceStatus: 'in-progress' })
// Through the metadata writes, so main's copy of the catalog agrees with the window's.
await store.getState().updateWorktreeMeta(first.id, { workspaceStatus: 'todo' })
await store.getState().updateWorktreeMeta(last.id, { workspaceStatus: 'completed' })
store.getState().setGroupBy('workspace-status')
store.setState({ collapsedGroups: new Set() })
return [first.id, `folder:${folder.id}`, last.id]
return [first.id, `folder:${folder.folderWorkspace.id}`, last.id]
}, folderPath)
expect(
@@ -79,7 +79,7 @@ test('a restart clears a tier nothing backs, keeps a hook row’s and an SSH hos
if (!folder) {
throw new Error('Expected a folder workspace')
}
return folder.id
return folder.folderWorkspace.id
}, folderPath)
folderToRemove = folderPath
const goneId = `folder:${goneFolderId}`
+1 -1
View File
@@ -59,7 +59,7 @@ test('main lists the window’s git and folder workspaces and follows its metada
if (!folder || !worktree) {
throw new Error('Expected a seeded worktree and a folder workspace')
}
return { worktreeId: worktree.id, folderKey: `local|folder:${folder.id}` }
return { worktreeId: worktree.id, folderKey: `local|folder:${folder.folderWorkspace.id}` }
}, folderPath)
const rowKey = `local|${worktreeId}`
@@ -39,9 +39,11 @@ for (const theme of ['light', 'dark'] as const) {
if (!folder) {
throw new Error('Could not create folder workspace')
}
await store.getState().updateFolderWorkspace(folder.id, { comment: 'Research synthesis' })
await store
.getState()
.updateFolderWorkspace(folder.folderWorkspace.id, { comment: 'Research synthesis' })
store.setState({ showSleepingWorkspaces: false })
return `folder:${folder.id}`
return `folder:${folder.folderWorkspace.id}`
},
{ folderPath, theme }
)
@@ -235,7 +235,7 @@ test.describe('Toggle Child Workspaces shortcut', () => {
if (!folder) {
throw new Error('Missing created folder workspace')
}
return `folder:${folder.id}`
return `folder:${folder.folderWorkspace.id}`
})
const folder = sidebarWorktreeRow(orcaPage, folderKey)
await expect(folder).toBeVisible()
+1 -1
View File
@@ -283,7 +283,7 @@ test.describe('Reveal active workspace button', () => {
if (!folder) {
throw new Error(`Folder workspace ${index} was not created`)
}
ids.push(folder.id)
ids.push(folder.folderWorkspace.id)
}
const targetKey = `folder:${ids[0]}` as const
store.setState({
+1 -1
View File
@@ -45,7 +45,7 @@ async function createFolders(page: Page, folders: { name: string; folderPath: st
if (!workspace) {
throw new Error(`Could not create folder workspace ${entry.name}`)
}
ids.push(workspace.id)
ids.push(workspace.folderWorkspace.id)
}
return ids
}, folders)