From a3ac517b8939f716b83b98d7be5d3793a9171287 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 12:30:12 -0700 Subject: [PATCH 1/2] fix: reveal send picker targets through host-owned ancestors --- .../src/components/sidebar/WorktreeList.tsx | 1 + .../agent-send-picker-collapse.test.tsx | 241 +++++++++++++++++ .../listing/use-collapsed-groups.ts | 74 ++--- .../navigation/pending-reveal-inputs.ts | 70 +---- .../navigation/worktree-reveal-group-keys.ts | 91 +++++++ tests/e2e/agent-send-picker-reveal.spec.ts | 255 ++++++++++++++++++ 6 files changed, 616 insertions(+), 116 deletions(-) create mode 100644 src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx create mode 100644 src/renderer/src/components/sidebar/worktree-list/navigation/worktree-reveal-group-keys.ts create mode 100644 tests/e2e/agent-send-picker-reveal.spec.ts diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 2b870c5ca899..c48e790add61 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -129,6 +129,7 @@ const WorktreeList = React.memo(function WorktreeList({ agentSendTargetWorktreeId, groupBy, pinnedDisplayPolicy, + worktrees: allWorktrees, visibleWorktrees, repoMap, worktreeMap, diff --git a/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx b/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx new file mode 100644 index 000000000000..e1bf5c5aea25 --- /dev/null +++ b/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx @@ -0,0 +1,241 @@ +// @vitest-environment happy-dom +import { cleanup, renderHook } from '@testing-library/react' +import { afterEach, expect, it } from 'vitest' +import { useAppStore } from '@/store' +import { makeRepo, makeWorktree } from './worktree-list-lineage-card-test-fixtures' +import { useEffectiveCollapsedGroups } from './worktree-list/listing/use-collapsed-groups' +import { useSidebarSectionRows } from './worktree-list/listing/use-section-rows' +import { getWorktreeLineageGroupKey } from './worktree-list/grouping/group-keys' +import { getWorktreeLineageAncestors } from './worktree-lineage-projection' +import type { WorktreeLineage } from '../../../../shared/worktree/lineage-types' +import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { WorktreeGroupBy } from './worktree-list/grouping/row-types' +import type { Worktree } from '../../../../shared/worktree/types' +import { cloneDefaultWorkspaceStatuses } from '../../../../shared/workspace-statuses' + +afterEach(cleanup) +const projectGrouping = { projects: [], projectHostSetups: [] } +const settings = useAppStore.getState().settings +const workspaceStatuses = cloneDefaultWorkspaceStatuses() +function setup(hostId: ExecutionHostId) { + const localRepo = makeRepo() + const remoteRepo = { ...localRepo, id: 'remote-repo', executionHostId: hostId } + const repoMap = new Map([ + [localRepo.id, localRepo], + [remoteRepo.id, remoteRepo] + ]) + const local = { + ...makeWorktree({ + id: 'local', + displayName: 'Local', + branch: 'local', + sortOrder: 0, + instanceId: 'local-instance' + }), + hostId: 'local' as const + } + const remote = { + ...local, + id: 'remote', + instanceId: 'remote-instance', + repoId: remoteRepo.id, + hostId + } + return { localRepo, remoteRepo, repoMap, local, remote } +} + +function sections( + hostId: ExecutionHostId, + groupBy: WorktreeGroupBy, + worktrees: Worktree[], + collapsedGroups: Set, + lineageById: Record = {} +) { + const { localRepo, remoteRepo, repoMap } = setup(hostId) + return renderHook(() => + useSidebarSectionRows({ + groupBy, + projectOrderBy: 'manual', + pinnedDisplayPolicy: 'single-location', + defaultHostId: 'local', + worktrees, + repos: [localRepo, remoteRepo], + repoMap, + worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])), + worktreeLineageById: lineageById, + prCache: null, + settings, + workspaceStatuses, + effectiveCollapsedGroups: collapsedGroups, + projectGrouping, + visibleReposForRows: [localRepo, remoteRepo], + visibleProjectGroupsForRows: [], + visibleFolderWorkspacesForRows: [], + importedWorktreesByRepo: new Map(), + newExternalWorktreesInboxByRepo: new Map(), + filterRepoIds: [], + visibleWorkspaceHostIds: ['local', hostId], + workspaceHostScope: 'all' + }) + ).result.current.sectionRows +} + +for (const hostId of ['ssh:builder', 'runtime:builder'] as const) { + it(`${hostId}: agent picker should expose a target within a collapsed host`, () => { + const { local, remote, repoMap } = setup(hostId) + const worktrees = [local, remote] + const collapsedGroups = new Set([`host:${hostId}`, 'host:local']) + const { result } = renderHook(() => + useEffectiveCollapsedGroups({ + hostScopedGroups: true, + collapsedGroups, + agentSendTargetWorktreeId: remote.id, + groupBy: 'none', + pinnedDisplayPolicy: 'single-location', + worktrees, + visibleWorktrees: worktrees, + repoMap, + worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])), + worktreeLineageById: {}, + prCache: null, + workspaceStatuses, + settings, + projectGroups: [], + projectGrouping, + folderWorkspaces: [], + defaultHostId: 'local' + }) + ) + expect([...collapsedGroups]).toEqual([`host:${hostId}`, 'host:local']) + expect(result.current.has('host:local')).toBe(true) + const rows = sections(hostId, 'none', worktrees, result.current) + expect(rows.some((row) => row.type === 'host-header' && row.hostId === hostId)).toBe(true) + expect( + sections(hostId, 'none', worktrees, new Set()).some( + (row) => row.type === 'item' && row.worktree.id === remote.id + ) + ).toBe(true) + expect(rows.some((row) => row.type === 'item' && row.worktree.id === remote.id)).toBe(true) + }) +} + +for (const hostId of ['local', 'ssh:builder', 'runtime:builder'] as const) { + it(`${hostId}: agent picker should expose child under a collapsed parent`, () => { + const { local, remote, repoMap } = setup(hostId) + const parent = hostId === 'local' ? local : remote + if (!parent.instanceId) { + throw new Error('Fixture parent instance missing') + } + const child = { ...parent, id: 'child', instanceId: 'child-instance', sortOrder: 1 } + const worktrees = [parent, child] + const lineage: WorktreeLineage = { + worktreeId: child.id, + worktreeInstanceId: child.instanceId, + parentWorktreeId: parent.id, + parentWorktreeInstanceId: parent.instanceId, + origin: 'orchestration', + capture: { source: 'orchestration-context', confidence: 'explicit' }, + createdAt: 1 + } + const worktreeLineageById = { [child.id]: lineage } + const worktreeMap = new Map(worktrees.map((worktree) => [worktree.id, worktree])) + expect(getWorktreeLineageAncestors(child, worktreeLineageById, worktreeMap)).toEqual([parent]) + const collapsedGroups = new Set([getWorktreeLineageGroupKey(parent)]) + expect( + sections(hostId, 'none', worktrees, collapsedGroups, worktreeLineageById).some( + (row) => row.type === 'item' && row.worktree.id === child.id + ) + ).toBe(false) + expect( + sections(hostId, 'none', worktrees, new Set(), worktreeLineageById).some( + (row) => row.type === 'item' && row.worktree.id === child.id + ) + ).toBe(true) + const { result } = renderHook(() => + useEffectiveCollapsedGroups({ + hostScopedGroups: true, + collapsedGroups, + agentSendTargetWorktreeId: child.id, + groupBy: 'none', + pinnedDisplayPolicy: 'single-location', + worktrees, + visibleWorktrees: worktrees, + repoMap, + worktreeMap, + worktreeLineageById, + prCache: null, + workspaceStatuses, + settings, + projectGroups: [], + projectGrouping, + folderWorkspaces: [], + defaultHostId: 'local' + }) + ) + expect(collapsedGroups.has(getWorktreeLineageGroupKey(parent))).toBe(true) + const after = sections(hostId, 'none', worktrees, result.current, worktreeLineageById) + expect(after.some((row) => row.type === 'item' && row.worktree.id === child.id)).toBe(true) + }) +} + +it('opens a filtered parent chain and owning lane, then restores the saved set on close', () => { + const { local, remote, repoMap } = setup('ssh:builder') + const root = { ...remote, instanceId: 'root-instance', isPinned: true } + const parent = { ...remote, id: 'parent', instanceId: 'parent-instance', isPinned: false } + const child = { ...remote, id: 'child', instanceId: 'child-instance', isPinned: false } + const worktrees = [local, root, parent, child] + const lineage = (worktree: typeof child, ancestor: typeof root): WorktreeLineage => ({ + worktreeId: worktree.id, + worktreeInstanceId: worktree.instanceId, + parentWorktreeId: ancestor.id, + parentWorktreeInstanceId: ancestor.instanceId, + origin: 'orchestration', + capture: { source: 'orchestration-context', confidence: 'explicit' }, + createdAt: 1 + }) + const collapsedGroups = new Set([ + 'host:ssh:builder', + 'lineage:ssh:builder|remote', + 'lineage:ssh:builder|parent', + 'all:host:ssh:builder', + 'all', + 'pinned:host:ssh:builder', + 'host:local', + 'lineage:local|parent' + ]) + const { result, rerender } = renderHook( + ({ targetId }: { targetId: string | null }) => + useEffectiveCollapsedGroups({ + hostScopedGroups: true, + collapsedGroups, + agentSendTargetWorktreeId: targetId, + groupBy: 'none', + pinnedDisplayPolicy: 'single-location', + worktrees, + visibleWorktrees: [local, child], + repoMap, + worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])), + worktreeLineageById: { + [parent.id]: lineage(parent, root), + [child.id]: lineage(child, parent) + }, + prCache: null, + workspaceStatuses, + settings, + projectGroups: [], + projectGrouping, + folderWorkspaces: [], + defaultHostId: 'local' + }), + { initialProps: { targetId: child.id } } + ) + expect([...result.current]).toEqual([ + 'all', + 'pinned:host:ssh:builder', + 'host:local', + 'lineage:local|parent' + ]) + expect(collapsedGroups.size).toBe(8) + rerender({ targetId: null }) + expect(result.current).toBe(collapsedGroups) +}) diff --git a/src/renderer/src/components/sidebar/worktree-list/listing/use-collapsed-groups.ts b/src/renderer/src/components/sidebar/worktree-list/listing/use-collapsed-groups.ts index e64fead9f266..1993416b0913 100644 --- a/src/renderer/src/components/sidebar/worktree-list/listing/use-collapsed-groups.ts +++ b/src/renderer/src/components/sidebar/worktree-list/listing/use-collapsed-groups.ts @@ -1,20 +1,15 @@ import { useMemo } from 'react' +import { getWorktreeRevealCollapsedGroupKeys } from '../navigation/worktree-reveal-group-keys' import type { AppState } from '@/store/types' import type { ProjectGroup } from '../../../../../../shared/project-group-types' import type { Repo } from '../../../../../../shared/repo-types' import type { WorkspaceStatusDefinition, Worktree } from '../../../../../../shared/worktree/types' import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types' -import { PINNED_GROUP_KEY, getLineageGroupKey } from '../grouping/group-keys' import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types' import type { ProjectGroupingModel } from '../grouping/project-grouping' -import { getGroupKeysForWorktree } from '../grouping/worktree-group-keys' import { getFolderWorkspaceRevealGroupKeys } from '../navigation/folder-reveal' import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types' import type { ExecutionHostId } from '../../../../../../shared/execution-host' -import { getWorktreeExecutionHostId } from '../../../../../../shared/execution-host' -import { getHostSectionCollapseKey } from '../../host-section-collapse' -import { isPinnedSectionWorktree } from '../../pinned-section-worktrees' -import { getWorktreeLineageAncestors } from '../../worktree-lineage-projection' // While the agent send picker targets a workspace, force open every section that hides it. export function useEffectiveCollapsedGroups(args: { @@ -23,6 +18,7 @@ export function useEffectiveCollapsedGroups(args: { agentSendTargetWorktreeId: string | null groupBy: WorktreeGroupBy pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + worktrees: readonly Worktree[] visibleWorktrees: readonly Worktree[] repoMap: Map worktreeMap: Map @@ -41,6 +37,7 @@ export function useEffectiveCollapsedGroups(args: { agentSendTargetWorktreeId, groupBy, pinnedDisplayPolicy, + worktrees, visibleWorktrees, repoMap, worktreeMap, @@ -77,54 +74,24 @@ export function useEffectiveCollapsedGroups(args: { return nextForFolder } const next = new Set(collapsedGroups) - if ( - pinnedDisplayPolicy === 'single-location' && - isPinnedSectionWorktree(targetWorktree, visibleWorktrees, worktreeLineageById, worktreeMap) - ) { - next.delete( - hostScopedGroups - ? getHostSectionCollapseKey( - PINNED_GROUP_KEY, - getWorktreeExecutionHostId( - targetWorktree, - repoMap.get(targetWorktree.repoId), - defaultHostId - ) - ) - : PINNED_GROUP_KEY - ) - } else { - for (const groupKey of getGroupKeysForWorktree( - groupBy, - targetWorktree, - repoMap, - prCache, - workspaceStatuses, - settings, - projectGroups, - projectGrouping - )) { - next.delete( - hostScopedGroups - ? getHostSectionCollapseKey( - groupKey, - getWorktreeExecutionHostId( - targetWorktree, - repoMap.get(targetWorktree.repoId), - defaultHostId - ) - ) - : groupKey - ) - } - } - - for (const parent of getWorktreeLineageAncestors( - targetWorktree, + for (const groupKey of getWorktreeRevealCollapsedGroupKeys({ + worktree: targetWorktree, + worktrees, + visibleWorktrees, worktreeLineageById, - worktreeMap - )) { - next.delete(getLineageGroupKey(parent.id)) + repoMap, + defaultHostId, + hostScopedGroups, + collapsedGroups, + groupBy, + pinnedDisplayPolicy, + prCache, + workspaceStatuses, + settings, + projectGroups, + projectGrouping + })) { + next.delete(groupKey) } return next }, [ @@ -133,6 +100,7 @@ export function useEffectiveCollapsedGroups(args: { hostScopedGroups, groupBy, pinnedDisplayPolicy, + worktrees, visibleWorktrees, prCache, projectGroups, diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/pending-reveal-inputs.ts b/src/renderer/src/components/sidebar/worktree-list/navigation/pending-reveal-inputs.ts index c377ce866e98..09d38d20ccff 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/pending-reveal-inputs.ts +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/pending-reveal-inputs.ts @@ -8,20 +8,11 @@ import type { Repo } from '../../../../../../shared/repo-types' import type { WorkspaceStatusDefinition, Worktree } from '../../../../../../shared/worktree/types' import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types' import type { ExecutionHostId } from '../../../../../../shared/execution-host' -import { getWorktreeExecutionHostId } from '../../../../../../shared/execution-host' import type { RenderRow } from '../listing/render-row' -import { getHostSectionCollapseKey } from '../../host-section-collapse' -import { getWorktreeLineageGroupKey, PINNED_GROUP_KEY } from '../grouping/group-keys' import type { ProjectGroupingModel } from '../grouping/project-grouping' import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types' -import { getGroupKeysForWorktree } from '../grouping/worktree-group-keys' -import { isPinnedSectionWorktree } from '../../pinned-section-worktrees' -import { - getHostScopedWorktreeLineageInputs, - getWorktreeLineageAncestors -} from '../../worktree-lineage-projection' import { getFolderWorkspaceRevealGroupKeys } from './folder-reveal' -import { getPinnedWorktreeRevealCollapsedGroupKeys } from './reveal-ancestors' +import { getWorktreeRevealCollapsedGroupKeys } from './worktree-reveal-group-keys' export const MAX_REVEAL_RETRIES = 8 @@ -91,59 +82,12 @@ export function expandGroupsForWorktreeReveal( if (!targetWorktree) { return } - const targetRepo = args.repoMap.get(targetWorktree.repoId) - const targetHostId = getWorktreeExecutionHostId(targetWorktree, targetRepo, args.defaultHostId) - const hostGroupKey = `host:${targetHostId}` - if (args.collapsedGroups.has(hostGroupKey)) { - args.toggleGroup(hostGroupKey) - } - - const hostLineage = getHostScopedWorktreeLineageInputs( - args.worktrees, - args.worktreeLineageById, - executionHostId - ) - for (const parent of getWorktreeLineageAncestors( - targetWorktree, - hostLineage.lineageById, - hostLineage.worktreeMap - )) { - const lineageGroupKey = getWorktreeLineageGroupKey(parent) - if (args.collapsedGroups.has(lineageGroupKey)) { - args.toggleGroup(lineageGroupKey) - } - } - - const groupKeys = - args.pinnedDisplayPolicy === 'single-location' && - isPinnedSectionWorktree( - targetWorktree, - args.worktrees, - args.worktreeLineageById, - args.worktreeMap - ) - ? getPinnedWorktreeRevealCollapsedGroupKeys({ - worktree: targetWorktree, - groupKey: hostScopedGroups - ? getHostSectionCollapseKey(PINNED_GROUP_KEY, targetHostId) - : undefined, - collapsedGroups: args.collapsedGroups, - inPinnedSection: true - }) - : getGroupKeysForWorktree( - args.groupBy, - targetWorktree, - args.repoMap, - args.prCache, - args.workspaceStatuses, - args.settings, - args.projectGroups, - args.projectGrouping - ).map((key) => (hostScopedGroups ? getHostSectionCollapseKey(key, targetHostId) : key)) - for (const groupKey of groupKeys) { - if (args.collapsedGroups.has(groupKey)) { - args.toggleGroup(groupKey) - } + for (const groupKey of getWorktreeRevealCollapsedGroupKeys({ + ...args, + worktree: targetWorktree, + hostScopedGroups + })) { + args.toggleGroup(groupKey) } } diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/worktree-reveal-group-keys.ts b/src/renderer/src/components/sidebar/worktree-list/navigation/worktree-reveal-group-keys.ts new file mode 100644 index 000000000000..0cfb78059007 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/worktree-reveal-group-keys.ts @@ -0,0 +1,91 @@ +import type { AppState } from '@/store/types' +import { + getWorktreeExecutionHostId, + type ExecutionHostId +} from '../../../../../../shared/execution-host' +import type { ProjectGroup } from '../../../../../../shared/project-group-types' +import type { Repo } from '../../../../../../shared/repo-types' +import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage-types' +import type { WorkspaceStatusDefinition, Worktree } from '../../../../../../shared/worktree/types' +import { getHostSectionCollapseKey } from '../../host-section-collapse' +import { isPinnedSectionWorktree } from '../../pinned-section-worktrees' +import { + getHostScopedWorktreeLineageInputs, + getWorktreeLineageAncestors +} from '../../worktree-lineage-projection' +import { getWorktreeLineageGroupKey, PINNED_GROUP_KEY } from '../grouping/group-keys' +import type { ProjectGroupingModel } from '../grouping/project-grouping' +import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types' +import { getGroupKeysForWorktree } from '../grouping/worktree-group-keys' +import { getPinnedWorktreeRevealCollapsedGroupKeys } from './reveal-ancestors' + +export function getWorktreeRevealCollapsedGroupKeys(args: { + worktree: Worktree + worktrees: readonly Worktree[] + visibleWorktrees?: readonly Worktree[] + worktreeLineageById: Readonly> + repoMap: Map + defaultHostId: ExecutionHostId + hostScopedGroups: boolean + collapsedGroups: ReadonlySet + groupBy: WorktreeGroupBy + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy + prCache: AppState['prCache'] | null + workspaceStatuses: readonly WorkspaceStatusDefinition[] + settings: AppState['settings'] + projectGroups: readonly ProjectGroup[] + projectGrouping?: ProjectGroupingModel +}): string[] { + const hostId = getWorktreeExecutionHostId( + args.worktree, + args.repoMap.get(args.worktree.repoId), + args.defaultHostId + ) + const hostLineage = getHostScopedWorktreeLineageInputs( + args.worktrees, + args.worktreeLineageById, + hostId + ) + const keys = [ + `host:${hostId}`, + ...getWorktreeLineageAncestors( + args.worktree, + hostLineage.lineageById, + hostLineage.worktreeMap + ).map(getWorktreeLineageGroupKey) + ] + const sectionKey = (key: string) => + args.hostScopedGroups ? getHostSectionCollapseKey(key, hostId) : key + if ( + args.pinnedDisplayPolicy === 'single-location' && + isPinnedSectionWorktree( + args.worktree, + args.visibleWorktrees ?? args.worktrees, + hostLineage.lineageById, + hostLineage.worktreeMap + ) + ) { + keys.push( + ...getPinnedWorktreeRevealCollapsedGroupKeys({ + worktree: args.worktree, + groupKey: sectionKey(PINNED_GROUP_KEY), + collapsedGroups: args.collapsedGroups, + inPinnedSection: true + }) + ) + } else { + keys.push( + ...getGroupKeysForWorktree( + args.groupBy, + args.worktree, + args.repoMap, + args.prCache, + args.workspaceStatuses, + args.settings, + args.projectGroups, + args.projectGrouping + ).map(sectionKey) + ) + } + return [...new Set(keys)].filter((key) => args.collapsedGroups.has(key)) +} diff --git a/tests/e2e/agent-send-picker-reveal.spec.ts b/tests/e2e/agent-send-picker-reveal.spec.ts new file mode 100644 index 000000000000..07468786cf53 --- /dev/null +++ b/tests/e2e/agent-send-picker-reveal.spec.ts @@ -0,0 +1,255 @@ +import { expect, test } from './helpers/orca-app' +import { + waitForActiveWorktree, + waitForSessionReady, + waitForStartupWorktreeRefresh +} from './helpers/store' + +for (const hostId of ['local', 'ssh:picker-builder'] as const) { + for (const hiddenBy of ['host', 'parent'] as const) { + test(`agent send picker reveals ${hostId} target hidden by ${hiddenBy} and restores collapse on close`, async ({ + orcaPage + }, testInfo) => { + await orcaPage.emulateMedia({ reducedMotion: 'reduce' }) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await waitForStartupWorktreeRefresh(orcaPage) + const ids = await orcaPage.evaluate( + async ({ hostId, hiddenBy }) => { + const store = window.__store! + const state = store.getState() + const sourceRepo = state.repos[0] + const source = state.worktreesByRepo[sourceRepo.id][0] + const remoteHostId = hostId === 'local' ? 'ssh:picker-builder' : hostId + const remoteRepo = { + ...sourceRepo, + id: 'picker-remote-repo', + displayName: 'Remote project', + executionHostId: remoteHostId, + connectionId: remoteHostId.startsWith('ssh:') ? 'picker-builder' : null + } + const local = { + ...source, + hostId: 'local' as const, + isPinned: false, + displayName: 'Local workspace' + } + const remote = { + ...source, + id: 'picker-remote-parent', + repoId: remoteRepo.id, + hostId: remoteHostId, + instanceId: 'remote-parent-instance', + isMainWorktree: false, + isPinned: false, + displayName: 'Remote workspace' + } + const parent = hostId === 'local' ? local : remote + const now = Date.now() + const localChild = state.worktreesByRepo[sourceRepo.id].find( + (worktree) => worktree.id !== source.id + ) + const child = + hostId === 'local' + ? localChild + : { + ...parent, + id: 'picker-child', + instanceId: 'picker-child-instance', + isMainWorktree: false + } + if (hiddenBy === 'parent' && !child) { + throw new Error('Seeded child workspace missing') + } + const targetId = hiddenBy === 'parent' ? child!.id : parent.id + const target = { + ...parent, + ...(hiddenBy === 'parent' ? child : {}), + displayName: hiddenBy === 'parent' ? 'Child workspace' : parent.displayName, + diffComments: [ + { + id: 'picker-note', + worktreeId: targetId, + filePath: 'README.md', + source: 'diff' as const, + lineNumber: 1, + body: 'Check sidebar disclosure', + side: 'modified' as const, + createdAt: now + } + ] + } + if (hostId === 'local') { + await state.updateWorktreeMeta( + target.id, + { + displayName: target.displayName, + isPinned: false, + diffComments: target.diffComments + }, + { executionHostId: 'local' } + ) + if (hiddenBy === 'parent') { + await state.assignWorktreeParent(target.id, { parentWorktreeId: parent.id }) + } + } + const localWorktrees = + hostId === 'local' ? (hiddenBy === 'parent' ? [local, target] : [target]) : [local] + const remoteWorktrees = + hostId !== 'local' ? (hiddenBy === 'parent' ? [remote, target] : [target]) : [remote] + const collapsedGroups = [ + hiddenBy === 'host' ? `host:${hostId}` : `lineage:${hostId}|${parent.id}` + ] + const worktreeLineageById = + hiddenBy === 'parent' + ? { + [target.id]: { + worktreeId: target.id, + worktreeInstanceId: target.instanceId!, + parentWorktreeId: parent.id, + parentWorktreeInstanceId: parent.instanceId!, + origin: 'orchestration' as const, + capture: { + source: 'orchestration-context' as const, + confidence: 'explicit' as const + }, + createdAt: now + } + } + : {} + const tabId = 'picker-agent-tab' + const leafId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' + const paneKey = `${tabId}:${leafId}` + const ptyId = 'picker-agent-pty' + await window.api.ui.set({ groupBy: 'none', collapsedGroups }) + state.setSidebarOpen(true) + state.setShowSleepingWorkspaces(true) + state.setHideDefaultBranchWorkspace(false) + state.setFilterRepoIds([]) + state.setRightSidebarTab('source-control') + state.setRightSidebarOpen(true) + store.setState({ + groupBy: 'none', + repos: [sourceRepo, remoteRepo], + worktreesByRepo: { [sourceRepo.id]: localWorktrees, [remoteRepo.id]: remoteWorktrees }, + worktreeLineageById, + visibleWorkspaceHostIds: ['local', remoteHostId], + workspaceHostScope: 'all', + activeRepoId: target.repoId, + activeWorktreeId: target.id, + activeWorkspaceKey: `worktree:${target.id}`, + activeWorkspaceExecutionHostId: hostId, + pendingRevealWorktree: null, + agentSendPopoverTargetMode: null, + collapsedGroups: new Set(collapsedGroups), + agentActivityDisplayMode: 'compact', + worktreeCardProperties: [ + ...state.worktreeCardProperties.filter((property) => property !== 'inline-agents'), + 'inline-agents' + ], + sshTargetLabels: new Map([['picker-builder', 'Remote builder']]), + sshConnectionStates: new Map([ + [ + 'picker-builder', + { + targetId: 'picker-builder', + status: 'connected', + error: null, + reconnectAttempt: 0 + } + ] + ]), + tabsByWorktree: { + ...state.tabsByWorktree, + [target.id]: [ + ...(state.tabsByWorktree[target.id] ?? []), + { + id: tabId, + worktreeId: target.id, + ptyId, + title: 'Claude', + customTitle: null, + color: null, + sortOrder: 1, + createdAt: now + } + ] + }, + terminalLayoutsByTabId: { + ...state.terminalLayoutsByTabId, + [tabId]: { + root: { type: 'leaf', leafId }, + activeLeafId: leafId, + expandedLeafId: null, + ptyIdsByLeafId: { [leafId]: ptyId } + } + }, + ptyIdsByTabId: { ...state.ptyIdsByTabId, [tabId]: [ptyId] }, + agentStatusByPaneKey: { + ...state.agentStatusByPaneKey, + [paneKey]: { + state: 'working', + prompt: 'Check sidebar disclosure', + updatedAt: now, + stateStartedAt: now, + agentType: 'claude', + paneKey, + worktreeId: target.id, + stateHistory: [] + } + } + }) + return { + target: target.id, + control: hostId === 'local' ? remote.id : local.id, + collapsedGroups + } + }, + { hostId, hiddenBy } + ) + const targetRow = orcaPage.locator( + `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.target)}]` + ) + const controlRow = orcaPage.locator( + `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.control)}]` + ) + await expect(targetRow).toHaveCount(0) + await expect(controlRow).toBeVisible() + const sendButton = orcaPage.getByRole('button', { + name: 'Send notes to an agent', + exact: true + }) + await expect(sendButton).toBeEnabled() + await orcaPage.screenshot({ + path: testInfo.outputPath('before-picker.png'), + clip: { x: 0, y: 170, width: 280, height: 500 } + }) + await sendButton.click() + await expect(orcaPage.getByText('Send notes to', { exact: true })).toBeVisible() + try { + await expect(targetRow).toBeVisible() + await expect(targetRow.locator('[data-agent-send-target="eligible"]')).toBeVisible() + } finally { + await orcaPage.screenshot({ + path: testInfo.outputPath('after-picker.png'), + clip: { x: 0, y: 170, width: 280, height: 500 } + }) + } + await expect(controlRow).toBeVisible() + await expect + .poll(() => orcaPage.evaluate(() => window.__store!.getState().pendingRevealWorktree)) + .toBeNull() + await orcaPage.keyboard.press('Escape') + await expect + .poll(() => orcaPage.evaluate(() => window.__store!.getState().agentSendPopoverTargetMode)) + .toBeNull() + await expect(targetRow).toHaveCount(0) + expect( + await orcaPage.evaluate(() => [...window.__store!.getState().collapsedGroups]) + ).toEqual(ids.collapsedGroups) + expect( + await orcaPage.evaluate(async () => (await window.api.ui.get()).collapsedGroups) + ).toEqual(ids.collapsedGroups) + }) + } +} From 207abb0189cc74ce804084d9dc700e48e8a18d7b Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 12:31:12 -0700 Subject: [PATCH 2/2] test: type picker close fixture as a nullable target --- .../src/components/sidebar/agent-send-picker-collapse.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx b/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx index e1bf5c5aea25..d96ead14e19f 100644 --- a/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx +++ b/src/renderer/src/components/sidebar/agent-send-picker-collapse.test.tsx @@ -203,6 +203,7 @@ it('opens a filtered parent chain and owning lane, then restores the saved set o 'host:local', 'lineage:local|parent' ]) + const initialProps: { targetId: string | null } = { targetId: child.id } const { result, rerender } = renderHook( ({ targetId }: { targetId: string | null }) => useEffectiveCollapsedGroups({ @@ -227,7 +228,7 @@ it('opens a filtered parent chain and owning lane, then restores the saved set o folderWorkspaces: [], defaultHostId: 'local' }), - { initialProps: { targetId: child.id } } + { initialProps } ) expect([...result.current]).toEqual([ 'all',