From 0ee7280ec9a264bdccab199c26c912bf39cd68f2 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:44:25 -0800 Subject: [PATCH] fix: align group workspace owners and create result consumers --- .../folder-workspace-connection-index.ts | 6 +++-- .../folder-workspace-path-status.test.ts | 27 +++++++++++++++++++ .../folder-workspace-removal.ts | 7 ++--- .../project-groups/project-group-mutations.ts | 4 +-- .../e2e/folder-workspace-palette-wake.spec.ts | 2 +- tests/e2e/paired-skill-installation.spec.ts | 2 +- tests/e2e/pdf-find-counter.spec.ts | 2 +- ...urce-manager-end-tabless-terminals.spec.ts | 2 +- .../resource-manager-folder-labels.spec.ts | 2 +- ...sidebar-folder-keyboard-navigation.spec.ts | 6 +++-- tests/e2e/sidebar-sort-stamps-startup.spec.ts | 2 +- tests/e2e/sidebar-workspace-rows.spec.ts | 2 +- ...ree-jump-palette-folder-workspaces.spec.ts | 6 +++-- .../worktree-lineage-toggle-shortcut.spec.ts | 2 +- tests/e2e/worktree-scroll-to-current.spec.ts | 2 +- tests/e2e/wsl-move-fence.spec.ts | 2 +- 16 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/main/project-groups/folder-workspace-connection-index.ts b/src/main/project-groups/folder-workspace-connection-index.ts index 935df693fa4..9b57a4a8157 100644 --- a/src/main/project-groups/folder-workspace-connection-index.ts +++ b/src/main/project-groups/folder-workspace-connection-index.ts @@ -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) { diff --git a/src/main/project-groups/folder-workspace-path-status.test.ts b/src/main/project-groups/folder-workspace-path-status.test.ts index 95ce2e4f3f1..36dc96528dc 100644 --- a/src/main/project-groups/folder-workspace-path-status.test.ts +++ b/src/main/project-groups/folder-workspace-path-status.test.ts @@ -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 { } 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) => { diff --git a/src/renderer/src/store/folder-workspaces/folder-workspace-removal.ts b/src/renderer/src/store/folder-workspaces/folder-workspace-removal.ts index ce185ff4c07..c18eb487514 100644 --- a/src/renderer/src/store/folder-workspaces/folder-workspace-removal.ts +++ b/src/renderer/src/store/folder-workspaces/folder-workspace-removal.ts @@ -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 diff --git a/src/renderer/src/store/project-groups/project-group-mutations.ts b/src/renderer/src/store/project-groups/project-group-mutations.ts index d27f010db7b..6b71ceeb273 100644 --- a/src/renderer/src/store/project-groups/project-group-mutations.ts +++ b/src/renderer/src/store/project-groups/project-group-mutations.ts @@ -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, diff --git a/tests/e2e/folder-workspace-palette-wake.spec.ts b/tests/e2e/folder-workspace-palette-wake.spec.ts index 289ca4572fa..1b17577e902 100644 --- a/tests/e2e/folder-workspace-palette-wake.spec.ts +++ b/tests/e2e/folder-workspace-palette-wake.spec.ts @@ -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 => { // Native menu accelerators need desktop focus; drive the same palette through CDP. diff --git a/tests/e2e/paired-skill-installation.spec.ts b/tests/e2e/paired-skill-installation.spec.ts index 0268dbb84a3..7682549a02c 100644 --- a/tests/e2e/paired-skill-installation.spec.ts +++ b/tests/e2e/paired-skill-installation.spec.ts @@ -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) } diff --git a/tests/e2e/pdf-find-counter.spec.ts b/tests/e2e/pdf-find-counter.spec.ts index d20358c7a87..560510959e9 100644 --- a/tests/e2e/pdf-find-counter.spec.ts +++ b/tests/e2e/pdf-find-counter.spec.ts @@ -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, diff --git a/tests/e2e/resource-manager-end-tabless-terminals.spec.ts b/tests/e2e/resource-manager-end-tabless-terminals.spec.ts index 06a19343937..f29599e6b7d 100644 --- a/tests/e2e/resource-manager-end-tabless-terminals.spec.ts +++ b/tests/e2e/resource-manager-end-tabless-terminals.spec.ts @@ -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 diff --git a/tests/e2e/resource-manager-folder-labels.spec.ts b/tests/e2e/resource-manager-folder-labels.spec.ts index 802195a6929..2288c3db46c 100644 --- a/tests/e2e/resource-manager-folder-labels.spec.ts +++ b/tests/e2e/resource-manager-folder-labels.spec.ts @@ -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) diff --git a/tests/e2e/sidebar-folder-keyboard-navigation.spec.ts b/tests/e2e/sidebar-folder-keyboard-navigation.spec.ts index e297655177e..8a41a644f3f 100644 --- a/tests/e2e/sidebar-folder-keyboard-navigation.spec.ts +++ b/tests/e2e/sidebar-folder-keyboard-navigation.spec.ts @@ -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( diff --git a/tests/e2e/sidebar-sort-stamps-startup.spec.ts b/tests/e2e/sidebar-sort-stamps-startup.spec.ts index db18c41eb58..c8c9b603f19 100644 --- a/tests/e2e/sidebar-sort-stamps-startup.spec.ts +++ b/tests/e2e/sidebar-sort-stamps-startup.spec.ts @@ -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}` diff --git a/tests/e2e/sidebar-workspace-rows.spec.ts b/tests/e2e/sidebar-workspace-rows.spec.ts index e9bf6ccc4c5..413a4e59075 100644 --- a/tests/e2e/sidebar-workspace-rows.spec.ts +++ b/tests/e2e/sidebar-workspace-rows.spec.ts @@ -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}` diff --git a/tests/e2e/worktree-jump-palette-folder-workspaces.spec.ts b/tests/e2e/worktree-jump-palette-folder-workspaces.spec.ts index e197f695854..4896a6d1491 100644 --- a/tests/e2e/worktree-jump-palette-folder-workspaces.spec.ts +++ b/tests/e2e/worktree-jump-palette-folder-workspaces.spec.ts @@ -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 } ) diff --git a/tests/e2e/worktree-lineage-toggle-shortcut.spec.ts b/tests/e2e/worktree-lineage-toggle-shortcut.spec.ts index 8c39e4265c6..28865af33b2 100644 --- a/tests/e2e/worktree-lineage-toggle-shortcut.spec.ts +++ b/tests/e2e/worktree-lineage-toggle-shortcut.spec.ts @@ -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() diff --git a/tests/e2e/worktree-scroll-to-current.spec.ts b/tests/e2e/worktree-scroll-to-current.spec.ts index 700e77ca8a4..d8765828b7d 100644 --- a/tests/e2e/worktree-scroll-to-current.spec.ts +++ b/tests/e2e/worktree-scroll-to-current.spec.ts @@ -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({ diff --git a/tests/e2e/wsl-move-fence.spec.ts b/tests/e2e/wsl-move-fence.spec.ts index 1780bfaf40f..985dd9da7bc 100644 --- a/tests/e2e/wsl-move-fence.spec.ts +++ b/tests/e2e/wsl-move-fence.spec.ts @@ -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)