From 67dba08620b8de6b1d10f49b1a4adb054fcfd707 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 11:58:10 -0700 Subject: [PATCH 1/3] fix(sidebar): scope lane collapse and reveal by execution host --- .../src/components/sidebar/WorktreeList.tsx | 2 +- .../sidebar/host-pinned-sections.test.ts | 84 --------- .../sidebar/host-pinned-sections.ts | 31 ---- .../host-section-collapse-folders.test.ts | 106 +++++++++++ .../sidebar/host-section-collapse.test.ts | 102 +++++++++++ .../sidebar/host-section-collapse.ts | 50 ++++++ .../sidebar/host-section-reveal.test.tsx | 169 ++++++++++++++++++ .../listing/use-collapsed-groups.ts | 28 ++- .../worktree-list/listing/use-section-rows.ts | 31 ++-- .../worktree-list/navigation/folder-reveal.ts | 18 +- .../navigation/pending-reveal-inputs.ts | 18 +- tests/e2e/worktree-reveal-host-pinned.spec.ts | 133 -------------- .../e2e/worktree-reveal-host-sections.spec.ts | 160 +++++++++++++++++ 13 files changed, 645 insertions(+), 287 deletions(-) delete mode 100644 src/renderer/src/components/sidebar/host-pinned-sections.test.ts delete mode 100644 src/renderer/src/components/sidebar/host-pinned-sections.ts create mode 100644 src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts create mode 100644 src/renderer/src/components/sidebar/host-section-collapse.test.ts create mode 100644 src/renderer/src/components/sidebar/host-section-collapse.ts create mode 100644 src/renderer/src/components/sidebar/host-section-reveal.test.tsx delete mode 100644 tests/e2e/worktree-reveal-host-pinned.spec.ts create mode 100644 tests/e2e/worktree-reveal-host-sections.spec.ts diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index c5f2dd7a4831..2b870c5ca899 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -123,7 +123,7 @@ const WorktreeList = React.memo(function WorktreeList({ agentSendTargetWorktreeId }) const effectiveCollapsedGroups = useEffectiveCollapsedGroups({ - hostScopedPinnedGroups: + hostScopedGroups: filterState.workspaceHostScope !== 'all' || filterState.visibleWorkspaceHostIds !== null, collapsedGroups, agentSendTargetWorktreeId, diff --git a/src/renderer/src/components/sidebar/host-pinned-sections.test.ts b/src/renderer/src/components/sidebar/host-pinned-sections.test.ts deleted file mode 100644 index 5c213e4d5a72..000000000000 --- a/src/renderer/src/components/sidebar/host-pinned-sections.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { makeRepo, makeWorktree } from './worktree-list-lineage-card-test-fixtures' -import { buildRows } from './worktree-list/grouping/build-rows' -import { addHostSectionRows } from './host-section-rows' -import { scopePinnedSectionCollapse } from './host-pinned-sections' - -for (const hostId of ['ssh:builder', 'runtime:builder'] as const) { - describe(`pinned sections on ${hostId}`, () => { - 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', - instanceId: 'local', - displayName: 'Local pin', - branch: 'local', - sortOrder: 0 - }), - hostId: 'local' as const, - isPinned: true - } - const remote = { ...local, id: 'remote', repoId: remoteRepo.id, hostId } - const rows = buildRows('none', [local, remote], repoMap, null, new Set()) - const sectioned = addHostSectionRows({ - rows, - hostOptions: [ - { id: 'local', kind: 'local', label: 'Local', detail: '', health: 'local' }, - { - id: hostId, - kind: hostId.startsWith('ssh:') ? 'ssh' : 'runtime', - label: 'Remote', - detail: '', - health: 'available' - } - ], - workspaceHostScope: 'all', - visibleWorkspaceHostIds: ['local', hostId], - defaultHostId: 'local' - }) - - it('keeps remote pins visible while local pins are collapsed', () => { - const result = scopePinnedSectionCollapse({ - rows: sectioned, - collapsedGroups: new Set(['pinned']), - defaultHostId: 'local' - }) - expect(result.filter((row) => row.type === 'item').map((row) => row.worktree.id)).toEqual([ - 'remote' - ]) - expect(result.filter((row) => row.type === 'header').map((row) => row.collapseKey)).toEqual([ - 'pinned', - `pinned:host:${hostId}` - ]) - }) - - it('keeps local pins visible while remote pins are collapsed', () => { - const result = scopePinnedSectionCollapse({ - rows: sectioned, - collapsedGroups: new Set([`pinned:host:${hostId}`]), - defaultHostId: 'local' - }) - expect(result.filter((row) => row.type === 'item').map((row) => row.worktree.id)).toEqual([ - 'local' - ]) - }) - - it('retains remote collapse ownership when it is the only visible host', () => { - const remoteRows = buildRows('none', [remote], repoMap, null, new Set()) - const result = scopePinnedSectionCollapse({ - rows: remoteRows, - collapsedGroups: new Set([`pinned:host:${hostId}`]), - defaultHostId: 'local' - }) - expect(result.filter((row) => row.type === 'item')).toEqual([]) - expect(result.filter((row) => row.type === 'header').map((row) => row.collapseKey)).toEqual([ - `pinned:host:${hostId}` - ]) - }) - }) -} diff --git a/src/renderer/src/components/sidebar/host-pinned-sections.ts b/src/renderer/src/components/sidebar/host-pinned-sections.ts deleted file mode 100644 index 5253aa5df0c9..000000000000 --- a/src/renderer/src/components/sidebar/host-pinned-sections.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getWorktreeExecutionHostId, type ExecutionHostId } from '../../../../shared/execution-host' -import type { HostSectionRow } from './host-section-rows' -import { PINNED_GROUP_KEY } from './worktree-list/grouping/group-keys' - -export function getHostPinnedGroupKey(hostId: ExecutionHostId): string { - return hostId === 'local' ? PINNED_GROUP_KEY : `${PINNED_GROUP_KEY}:host:${hostId}` -} - -export function scopePinnedSectionCollapse(args: { - rows: readonly HostSectionRow[] - collapsedGroups: ReadonlySet - defaultHostId: ExecutionHostId -}): HostSectionRow[] { - return args.rows.flatMap((row): HostSectionRow[] => { - if (row.type === 'header' && row.key === PINNED_GROUP_KEY) { - const hostId = - row.hostId ?? - (row.hostWorktreeCounts?.size === 1 - ? row.hostWorktreeCounts.keys().next().value - : undefined) - return [{ ...row, collapseKey: hostId ? getHostPinnedGroupKey(hostId) : row.key }] - } - if (row.type === 'item' && row.sectionKey === PINNED_GROUP_KEY) { - const hostId = getWorktreeExecutionHostId(row.worktree, row.repo, args.defaultHostId) - if (args.collapsedGroups.has(getHostPinnedGroupKey(hostId))) { - return [] - } - } - return [row] - }) -} diff --git a/src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts b/src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts new file mode 100644 index 000000000000..c2382e93fceb --- /dev/null +++ b/src/renderer/src/components/sidebar/host-section-collapse-folders.test.ts @@ -0,0 +1,106 @@ +import { expect, it } from 'vitest' +import type { ProjectGroup } from '../../../../shared/project-group-types' +import { makeFolderWorkspace } from '../../store/slices/worktrees-slice-test-fixtures' +import { folderWorkspaceKey } from '../../../../shared/workspace-scope' +import { buildRows } from './worktree-list/grouping/build-rows' +import { addHostSectionRows } from './host-section-rows' +import { scopeHostSectionCollapse } from './host-section-collapse' +import { getFolderWorkspaceRevealGroupKeys } from './worktree-list/navigation/folder-reveal' + +const localGroup: ProjectGroup = { + id: 'local-group', + name: 'Local', + parentPath: '/workspace/local', + parentGroupId: null, + createdFrom: 'manual', + tabOrder: 0, + isCollapsed: false, + color: null, + createdAt: 1, + updatedAt: 1 +} +const remoteGroup = { ...localGroup, id: 'remote-group', connectionId: 'builder' } +const local = makeFolderWorkspace({ id: 'local-folder', projectGroupId: localGroup.id }) +const remote = makeFolderWorkspace({ + id: 'remote-folder', + projectGroupId: remoteGroup.id, + connectionId: 'builder' +}) + +for (const [groupBy, key] of [ + ['none', 'all'], + ['workspace-status', 'workspace-status:in-progress'], + ['pr-status', 'pr:in-progress'] +] as const) { + const rows = buildRows( + groupBy, + [], + new Map(), + null, + new Set(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + [localGroup, remoteGroup], + undefined, + undefined, + undefined, + undefined, + undefined, + [local, remote] + ) + const sectioned = addHostSectionRows({ + rows, + hostOptions: [ + { id: 'local', kind: 'local', label: 'Local', detail: '', health: 'local' }, + { id: 'ssh:builder', kind: 'ssh', label: 'Remote', detail: '', health: 'available' } + ], + workspaceHostScope: 'all', + visibleWorkspaceHostIds: ['local', 'ssh:builder'], + defaultHostId: 'local' + }) + + it(`${groupBy}: collapses folder workspaces only on the owning host`, () => { + const collapsed = scopeHostSectionCollapse({ rows: sectioned, collapsedGroups: new Set([key]) }) + expect( + collapsed + .filter((row) => row.type === 'folder-workspace') + .map((row) => row.folderWorkspace.id) + ).toEqual([remote.id]) + const remoteCollapsed = scopeHostSectionCollapse({ + rows: sectioned, + collapsedGroups: new Set([`${key}:host:ssh:builder`]) + }) + expect( + remoteCollapsed + .filter((row) => row.type === 'folder-workspace') + .map((row) => row.folderWorkspace.id) + ).toEqual([local.id]) + }) + + it(`${groupBy}: reveals a remote folder using the rendered host lane key`, () => { + const keys = getFolderWorkspaceRevealGroupKeys( + folderWorkspaceKey(remote.id), + [local, remote], + [localGroup, remoteGroup], + { + groupBy, + defaultHostId: 'local', + hostScopedGroups: true + } + ) + expect(keys).toContain(`${key}:host:ssh:builder`) + expect(keys).toContain('host:ssh:builder') + expect(keys).not.toContain(key) + expect( + getFolderWorkspaceRevealGroupKeys(folderWorkspaceKey(remote.id), [remote], [remoteGroup], { + groupBy, + defaultHostId: 'local' + }) + ).toContain(key) + }) +} diff --git a/src/renderer/src/components/sidebar/host-section-collapse.test.ts b/src/renderer/src/components/sidebar/host-section-collapse.test.ts new file mode 100644 index 000000000000..14625b1e67b7 --- /dev/null +++ b/src/renderer/src/components/sidebar/host-section-collapse.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest' +import { makeRepo, makeWorktree } from './worktree-list-lineage-card-test-fixtures' +import { buildRows } from './worktree-list/grouping/build-rows' +import { addHostSectionRows } from './host-section-rows' +import { scopeHostSectionCollapse, deferHostSectionCollapse } from './host-section-collapse' + +for (const [groupBy, sectionKey, pinned] of [ + ['none', 'pinned', true], + ['none', 'all', false], + ['workspace-status', 'workspace-status:in-progress', false], + ['pr-status', 'pr:in-progress', false] +] as const) { + for (const hostId of ['ssh:builder', 'runtime:builder'] as const) { + describe(`${sectionKey} sections on ${hostId}`, () => { + 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', + instanceId: 'local', + displayName: 'Local pin', + branch: 'local', + sortOrder: 0 + }), + hostId: 'local' as const, + isPinned: pinned + } + const remote = { ...local, id: 'remote', repoId: remoteRepo.id, hostId } + const rows = buildRows(groupBy, [local, remote], repoMap, null, new Set()) + const sectioned = addHostSectionRows({ + rows, + hostOptions: [ + { id: 'local', kind: 'local', label: 'Local', detail: '', health: 'local' }, + { + id: hostId, + kind: hostId.startsWith('ssh:') ? 'ssh' : 'runtime', + label: 'Remote', + detail: '', + health: 'available' + } + ], + workspaceHostScope: 'all', + visibleWorkspaceHostIds: ['local', hostId], + defaultHostId: 'local' + }) + + it('keeps remote pins visible while local pins are collapsed', () => { + const result = scopeHostSectionCollapse({ + rows: sectioned, + collapsedGroups: new Set([sectionKey]) + }) + expect(result.filter((row) => row.type === 'item').map((row) => row.worktree.id)).toEqual([ + 'remote' + ]) + expect(result.filter((row) => row.type === 'header').map((row) => row.collapseKey)).toEqual( + [sectionKey, `${sectionKey}:host:${hostId}`] + ) + }) + + it('keeps local pins visible while remote pins are collapsed', () => { + const result = scopeHostSectionCollapse({ + rows: sectioned, + collapsedGroups: new Set([`${sectionKey}:host:${hostId}`]) + }) + expect(result.filter((row) => row.type === 'item').map((row) => row.worktree.id)).toEqual([ + 'local' + ]) + }) + + it('retains remote collapse ownership when it is the only visible host', () => { + const remoteRows = buildRows(groupBy, [remote], repoMap, null, new Set()) + const result = scopeHostSectionCollapse({ + rows: remoteRows, + collapsedGroups: new Set([`${sectionKey}:host:${hostId}`]) + }) + expect(result.filter((row) => row.type === 'item')).toEqual([]) + expect(result.filter((row) => row.type === 'header').map((row) => row.collapseKey)).toEqual( + [`${sectionKey}:host:${hostId}`] + ) + }) + }) + } +} + +it('defers only lane collapse and retains unrelated project/lineage collapse', () => { + const groups = new Set([ + 'pinned', + 'all', + 'pr:in-progress', + 'workspace-status:done', + 'repo:1', + 'lineage:local|parent' + ]) + expect([...deferHostSectionCollapse(groups)]).toEqual(['repo:1', 'lineage:local|parent']) + expect(groups.size).toBe(6) + const unchanged = new Set(['repo:1']) + expect(deferHostSectionCollapse(unchanged)).toBe(unchanged) +}) diff --git a/src/renderer/src/components/sidebar/host-section-collapse.ts b/src/renderer/src/components/sidebar/host-section-collapse.ts new file mode 100644 index 000000000000..0275f2a705ce --- /dev/null +++ b/src/renderer/src/components/sidebar/host-section-collapse.ts @@ -0,0 +1,50 @@ +import type { ExecutionHostId } from '../../../../shared/execution-host' +import type { HostSectionRow } from './host-section-rows' +import { PINNED_GROUP_KEY } from './worktree-list/grouping/group-keys' + +export function isHostLaneSectionKey(key: string): boolean { + return ( + key === PINNED_GROUP_KEY || + key === 'all' || + key.startsWith('pr:') || + key.startsWith('workspace-status:') + ) +} + +export function getHostSectionCollapseKey(key: string, hostId: ExecutionHostId): string { + return hostId === 'local' || !isHostLaneSectionKey(key) ? key : `${key}:host:${hostId}` +} + +export function deferHostSectionCollapse(collapsedGroups: Set): Set { + if (![...collapsedGroups].some(isHostLaneSectionKey)) { + return collapsedGroups + } + return new Set([...collapsedGroups].filter((key) => !isHostLaneSectionKey(key))) +} + +export function scopeHostSectionCollapse(args: { + rows: readonly HostSectionRow[] + collapsedGroups: ReadonlySet +}): HostSectionRow[] { + let sectionCollapsed = false + return args.rows.flatMap((row): HostSectionRow[] => { + if (row.type === 'host-header') { + sectionCollapsed = false + } + if (row.type === 'header') { + sectionCollapsed = false + if (!isHostLaneSectionKey(row.key)) { + return [row] + } + const hostId = + row.hostId ?? + (row.hostWorktreeCounts?.size === 1 + ? row.hostWorktreeCounts.keys().next().value + : undefined) + const collapseKey = hostId ? getHostSectionCollapseKey(row.key, hostId) : row.key + sectionCollapsed = args.collapsedGroups.has(collapseKey) + return [{ ...row, collapseKey }] + } + return sectionCollapsed ? [] : [row] + }) +} diff --git a/src/renderer/src/components/sidebar/host-section-reveal.test.tsx b/src/renderer/src/components/sidebar/host-section-reveal.test.tsx new file mode 100644 index 000000000000..ddc5278cfd83 --- /dev/null +++ b/src/renderer/src/components/sidebar/host-section-reveal.test.tsx @@ -0,0 +1,169 @@ +// @vitest-environment happy-dom +import { createRef } from 'react' +import { cleanup, renderHook } from '@testing-library/react' +import { Virtualizer } from '@tanstack/react-virtual' +import { afterEach, expect, it, vi } from 'vitest' +import { useAppStore } from '@/store' +import { makeRepo, makeWorktree } from './worktree-list-lineage-card-test-fixtures' +import { useSidebarSectionRows } from './worktree-list/listing/use-section-rows' +import { expandGroupsForWorktreeReveal } from './worktree-list/navigation/pending-reveal-inputs' +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() +const virtualizer = new Virtualizer({ + count: 0, + getScrollElement: () => null, + estimateSize: () => 40, + scrollToFn: vi.fn(), + observeElementRect: vi.fn(), + observeElementOffset: vi.fn() +}) + +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 = {}, + visibleWorkspaceHostIds: ExecutionHostId[] | null = ['local', hostId] +) { + 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, + workspaceHostScope: 'all' + }) + ).result.current.sectionRows +} + +for (const hostId of ['ssh:builder', 'runtime:builder'] as const) { + for (const [groupBy, laneKey] of [ + ['none', 'all'], + ['workspace-status', 'workspace-status:in-progress'], + ['pr-status', 'pr:in-progress'] + ] as const) { + it(`${hostId} ${groupBy}: revealing remote should preserve collapsed local lane`, () => { + const { local, remote, repoMap } = setup(hostId) + const worktrees = [local, remote] + const collapsedGroups = new Set([laneKey, `${laneKey}:host:${hostId}`]) + const rows = sections(hostId, groupBy, worktrees, collapsedGroups) + expect(rows.filter((row) => row.type === 'header' && row.key === laneKey)).toHaveLength(2) + expect(rows.filter((row) => row.type === 'item')).toHaveLength(0) + expandGroupsForWorktreeReveal( + { + pendingRevealWorktree: null, + pendingRevealSidebarRow: null, + clearPendingRevealWorktreeId: vi.fn(), + clearPendingRevealSidebarRow: vi.fn(), + agentSendTargetWorktreeId: null, + renderRows: rows, + virtualizer, + scrollRef: createRef(), + worktrees, + folderWorkspaces: [], + repoMap, + worktreeMap: new Map(worktrees.map((worktree) => [worktree.id, worktree])), + worktreeLineageById: {}, + collapsedGroups, + toggleGroup: (key) => { + if (collapsedGroups.has(key)) { + collapsedGroups.delete(key) + } else { + collapsedGroups.add(key) + } + }, + groupBy, + pinnedDisplayPolicy: 'single-location', + defaultHostId: 'local', + prCache: null, + workspaceStatuses, + settings, + projectGroups: [], + projectGrouping, + flashRevealedRow: vi.fn(), + markRevealScroll: vi.fn(), + schedulePendingRevealFrame: vi.fn(), + cancelPendingRevealFrames: vi.fn() + }, + remote.id, + hostId + ) + const after = sections(hostId, groupBy, worktrees, collapsedGroups) + expect(after.some((row) => row.type === 'item' && row.worktree.id === remote.id)).toBe(true) + expect(after.some((row) => row.type === 'item' && row.worktree.id === local.id)).toBe(false) + }) + it(`${hostId} ${groupBy}: combined view retains its single section collapse key`, () => { + const { local, remote } = setup(hostId) + const worktrees = [local, remote] + const rows = sections(hostId, groupBy, worktrees, new Set([laneKey]), {}, null) + expect(rows.filter((row) => row.type === 'header' && row.key === laneKey)).toHaveLength(1) + expect(rows.some((row) => row.type === 'item')).toBe(false) + const expanded = sections( + hostId, + groupBy, + worktrees, + new Set([`${laneKey}:host:${hostId}`]), + {}, + null + ) + expect(expanded.filter((row) => row.type === 'item')).toHaveLength(2) + expect(expanded.some((row) => row.type === 'header' && row.collapseKey !== undefined)).toBe( + false + ) + }) + } +} 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 6918a1162cea..e64fead9f266 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 @@ -12,13 +12,13 @@ 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 { getHostPinnedGroupKey } from '../../host-pinned-sections' +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: { - hostScopedPinnedGroups?: boolean + hostScopedGroups?: boolean collapsedGroups: Set agentSendTargetWorktreeId: string | null groupBy: WorktreeGroupBy @@ -37,7 +37,7 @@ export function useEffectiveCollapsedGroups(args: { }): Set { const { collapsedGroups, - hostScopedPinnedGroups = false, + hostScopedGroups = false, agentSendTargetWorktreeId, groupBy, pinnedDisplayPolicy, @@ -65,7 +65,7 @@ export function useEffectiveCollapsedGroups(args: { agentSendTargetWorktreeId, folderWorkspaces, projectGroups, - { groupBy, workspaceStatuses, defaultHostId } + { groupBy, workspaceStatuses, defaultHostId, hostScopedGroups } ) if (folderKeys.length === 0) { return collapsedGroups @@ -82,8 +82,9 @@ export function useEffectiveCollapsedGroups(args: { isPinnedSectionWorktree(targetWorktree, visibleWorktrees, worktreeLineageById, worktreeMap) ) { next.delete( - hostScopedPinnedGroups - ? getHostPinnedGroupKey( + hostScopedGroups + ? getHostSectionCollapseKey( + PINNED_GROUP_KEY, getWorktreeExecutionHostId( targetWorktree, repoMap.get(targetWorktree.repoId), @@ -103,7 +104,18 @@ export function useEffectiveCollapsedGroups(args: { projectGroups, projectGrouping )) { - next.delete(groupKey) + next.delete( + hostScopedGroups + ? getHostSectionCollapseKey( + groupKey, + getWorktreeExecutionHostId( + targetWorktree, + repoMap.get(targetWorktree.repoId), + defaultHostId + ) + ) + : groupKey + ) } } @@ -118,7 +130,7 @@ export function useEffectiveCollapsedGroups(args: { }, [ agentSendTargetWorktreeId, collapsedGroups, - hostScopedPinnedGroups, + hostScopedGroups, groupBy, pinnedDisplayPolicy, visibleWorktrees, diff --git a/src/renderer/src/components/sidebar/worktree-list/listing/use-section-rows.ts b/src/renderer/src/components/sidebar/worktree-list/listing/use-section-rows.ts index 34c1232e5ce0..140fa128cef6 100644 --- a/src/renderer/src/components/sidebar/worktree-list/listing/use-section-rows.ts +++ b/src/renderer/src/components/sidebar/worktree-list/listing/use-section-rows.ts @@ -16,8 +16,7 @@ import type { ProjectGroupingModel } from '../grouping/project-grouping' import type { PinnedWorktreeDisplayPolicy, Row, WorktreeGroupBy } from '../grouping/row-types' import { getLogicalRepoOrderRankById } from '../../project-header-drop' import { getEmptyProjectPlaceholderRepoIds } from '../../empty-project-placeholder-repos' -import { PINNED_GROUP_KEY } from '../grouping/group-keys' -import { scopePinnedSectionCollapse } from '../../host-pinned-sections' +import { deferHostSectionCollapse, scopeHostSectionCollapse } from '../../host-section-collapse' import { addHostSectionRows } from '../../host-section-rows' import { orderHostSectionOptions } from '../../host-section-order' import { buildSidebarHostOptions } from '../../sidebar-host-options' @@ -143,17 +142,16 @@ export function useSidebarSectionRows(args: SectionRowsArgs) { [hostOptions] ) - const hostScopedPinnedGroups = + const hostScopedGroups = args.workspaceHostScope !== 'all' || args.visibleWorkspaceHostIds !== null - // Host sections apply pinned collapse after splitting the rows by owner. - const rowCollapsedGroups = useMemo(() => { - if (!hostScopedPinnedGroups || !effectiveCollapsedGroups.has(PINNED_GROUP_KEY)) { - return effectiveCollapsedGroups - } - const next = new Set(effectiveCollapsedGroups) - next.delete(PINNED_GROUP_KEY) - return next - }, [effectiveCollapsedGroups, hostScopedPinnedGroups]) + // Host sections apply lane collapse after splitting the rows by owner. + const rowCollapsedGroups = useMemo( + () => + hostScopedGroups + ? deferHostSectionCollapse(effectiveCollapsedGroups) + : effectiveCollapsedGroups, + [effectiveCollapsedGroups, hostScopedGroups] + ) const rows: Row[] = useMemo( () => @@ -241,11 +239,10 @@ export function useSidebarSectionRows(args: SectionRowsArgs) { // Why: projects/workspaces are the primary sidebar object; host sections are only an explicit host-filter view. preferProjectGrouping: true }) - return hostScopedPinnedGroups - ? scopePinnedSectionCollapse({ + return hostScopedGroups + ? scopeHostSectionCollapse({ rows: sectioned, - collapsedGroups: effectiveCollapsedGroups, - defaultHostId + collapsedGroups: effectiveCollapsedGroups }) : sectioned }, [ @@ -254,7 +251,7 @@ export function useSidebarSectionRows(args: SectionRowsArgs) { defaultHostId, effectiveCollapsedGroups, hostDragActive, - hostScopedPinnedGroups, + hostScopedGroups, orderedHostOptions, rows ]) diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.ts b/src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.ts index fd6766e1c4a3..4be37810c8ee 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.ts +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/folder-reveal.ts @@ -8,6 +8,7 @@ import type { ExecutionHostId } from '../../../../../../shared/execution-host' import { getFolderWorkspaceLaneKey } from '../grouping/folder-workspace-lanes' import type { WorktreeGroupBy } from '../grouping/row-types' import { getFolderWorkspaceHostId } from '../../folder-workspace-host-id' +import { getHostSectionCollapseKey } from '../../host-section-collapse' function findFolderWorkspaceByKey( worktreeId: string, @@ -65,6 +66,7 @@ export function getFolderWorkspaceRevealGroupKeys( groupBy?: WorktreeGroupBy workspaceStatuses?: readonly WorkspaceStatusDefinition[] defaultHostId?: ExecutionHostId + hostScopedGroups?: boolean } ): string[] { const folderWorkspace = findFolderWorkspaceByKey(worktreeId, folderWorkspaces) @@ -91,12 +93,18 @@ export function getFolderWorkspaceRevealGroupKeys( // keys come from the same function grouping uses, so the two cannot disagree. const owningGroup = groupsById.get(folderWorkspace.projectGroupId) if (options?.groupBy && options.groupBy !== 'repo' && owningGroup) { + const laneKey = getFolderWorkspaceLaneKey( + { folderWorkspace, projectGroup: owningGroup }, + options.groupBy, + options.workspaceStatuses ?? [] + ) keys.push( - getFolderWorkspaceLaneKey( - { folderWorkspace, projectGroup: owningGroup }, - options.groupBy, - options.workspaceStatuses ?? [] - ) + options.hostScopedGroups && options.defaultHostId + ? getHostSectionCollapseKey( + laneKey, + getFolderWorkspaceHostId(folderWorkspace, owningGroup, options.defaultHostId) + ) + : laneKey ) } if (owningGroup && options?.defaultHostId) { 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 91f1d43871ea..0fb42e525d95 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 @@ -10,7 +10,8 @@ import type { WorktreeLineage } from '../../../../../../shared/worktree/lineage- import type { ExecutionHostId } from '../../../../../../shared/execution-host' import { getWorktreeExecutionHostId } from '../../../../../../shared/execution-host' import type { RenderRow } from '../listing/render-row' -import { getHostPinnedGroupKey } from '../../host-pinned-sections' +import { getHostSectionCollapseKey } from '../../host-section-collapse' +import { PINNED_GROUP_KEY } from '../grouping/group-keys' import { getWorktreeLineageGroupKey } from '../grouping/group-keys' import type { ProjectGroupingModel } from '../grouping/project-grouping' import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types' @@ -61,6 +62,9 @@ export function expandGroupsForWorktreeReveal( worktreeId: string, executionHostId?: ExecutionHostId ): void { + const hostScopedGroups = args.renderRows.some( + (row) => row.type === 'host-header' || (row.type === 'header' && row.collapseKey !== undefined) + ) const folderGroupKeys = getFolderWorkspaceRevealGroupKeys( worktreeId, args.folderWorkspaces, @@ -68,7 +72,8 @@ export function expandGroupsForWorktreeReveal( { groupBy: args.groupBy, workspaceStatuses: args.workspaceStatuses, - defaultHostId: args.defaultHostId + defaultHostId: args.defaultHostId, + hostScopedGroups } ) if (folderGroupKeys.length > 0) { @@ -120,11 +125,8 @@ export function expandGroupsForWorktreeReveal( ) ? getPinnedWorktreeRevealCollapsedGroupKeys({ worktree: targetWorktree, - groupKey: args.renderRows.some( - (row) => - row.type === 'host-header' || (row.type === 'header' && row.collapseKey !== undefined) - ) - ? getHostPinnedGroupKey(targetHostId) + groupKey: hostScopedGroups + ? getHostSectionCollapseKey(PINNED_GROUP_KEY, targetHostId) : undefined, collapsedGroups: args.collapsedGroups, inPinnedSection: true @@ -138,7 +140,7 @@ export function expandGroupsForWorktreeReveal( 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) diff --git a/tests/e2e/worktree-reveal-host-pinned.spec.ts b/tests/e2e/worktree-reveal-host-pinned.spec.ts deleted file mode 100644 index b8d586663a7d..000000000000 --- a/tests/e2e/worktree-reveal-host-pinned.spec.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { expect, test } from './helpers/orca-app' -import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' - -for (const hostId of ['ssh:reveal-builder', 'runtime:reveal-builder'] as const) { - for (const collapsedHost of [false, true]) { - test(`reveals ${hostId} pinned workspace without expanding local pins (host collapsed: ${collapsedHost})`, async ({ - orcaPage - }, testInfo) => { - await orcaPage.emulateMedia({ reducedMotion: 'reduce' }) - await waitForSessionReady(orcaPage) - await waitForActiveWorktree(orcaPage) - const ids = await orcaPage.evaluate( - async ({ hostId, collapsedHost }) => { - const store = window.__store! - const state = store.getState() - const sourceRepo = state.repos[0] - const sourceWorktree = state.worktreesByRepo[sourceRepo.id][0] - await state.updateWorktreeMeta(sourceWorktree.id, { - isPinned: true, - displayName: 'Local pinned workspace' - }) - const local = { - ...sourceWorktree, - hostId: 'local' as const, - isPinned: true, - displayName: 'Local pinned workspace' - } - const remoteRepo = { - ...sourceRepo, - id: 'reveal-remote-repo', - executionHostId: hostId, - connectionId: hostId.startsWith('ssh:') ? 'reveal-builder' : null, - displayName: 'Remote project' - } - const remote = { - ...sourceWorktree, - id: 'reveal-remote-workspace', - repoId: remoteRepo.id, - hostId, - isPinned: true, - isMainWorktree: false, - displayName: 'Remote pinned workspace' - } - state.setActiveView('terminal') - state.setSidebarOpen(true) - await window.api.ui.set({ groupBy: 'none' }) - state.setShowSleepingWorkspaces(true) - state.setHideDefaultBranchWorkspace(false) - state.setFilterRepoIds([]) - const collapsedGroups = [ - 'pinned', - `pinned:host:${hostId}`, - ...(collapsedHost ? [`host:${hostId}`] : []) - ] - await window.api.ui.set({ collapsedGroups }) - store.setState({ - groupBy: 'none', - repos: [sourceRepo, remoteRepo], - worktreesByRepo: { [sourceRepo.id]: [local], [remoteRepo.id]: [remote] }, - sshTargetLabels: new Map([['reveal-builder', 'Remote builder']]), - sshConnectionStates: new Map([ - [ - 'reveal-builder', - { - targetId: 'reveal-builder', - status: 'connected', - error: null, - reconnectAttempt: 0 - } - ] - ]), - visibleWorkspaceHostIds: ['local', hostId], - workspaceHostScope: 'all', - activeRepoId: remoteRepo.id, - activeWorktreeId: remote.id, - activeWorkspaceKey: `worktree:${remote.id}`, - activeWorkspaceExecutionHostId: hostId, - pendingRevealWorktree: null, - collapsedGroups: new Set(collapsedGroups) - }) - return { local: local.id, remote: remote.id } - }, - { hostId, collapsedHost } - ) - const pins = orcaPage.getByRole('button', { name: /^Pinned/ }) - const localRow = orcaPage.locator( - `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.local)}]` - ) - const remoteRow = orcaPage.locator( - `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.remote)}]` - ) - await expect(pins.first()).toHaveAttribute('aria-expanded', 'false') - await expect(localRow).toHaveCount(0) - await expect(remoteRow).toHaveCount(0) - await orcaPage.screenshot({ - path: testInfo.outputPath('before-reveal.png'), - clip: { x: 0, y: 170, width: 280, height: 500 } - }) - await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() - await expect(remoteRow).toBeVisible() - await expect(remoteRow).toHaveAttribute('data-scroll-reveal-highlight', 'true') - await orcaPage.screenshot({ - path: testInfo.outputPath('after-reveal.png'), - clip: { x: 0, y: 170, width: 280, height: 500 } - }) - await expect(pins).toHaveCount(2) - await expect(pins.first()).toHaveAttribute('aria-expanded', 'false') - await expect(pins.last()).toHaveAttribute('aria-expanded', 'true') - await expect(localRow).toHaveCount(0) - - await pins.last().click() - await expect(remoteRow).toHaveCount(0) - await orcaPage.evaluate((localId) => { - const store = window.__store! - const local = Object.values(store.getState().worktreesByRepo) - .flat() - .find((worktree) => worktree.id === localId)! - store.setState({ - activeRepoId: local.repoId, - activeWorktreeId: localId, - activeWorkspaceKey: `worktree:${localId}`, - activeWorkspaceExecutionHostId: 'local', - pendingRevealWorktree: null - }) - }, ids.local) - await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() - await expect(localRow).toBeVisible() - await expect(pins.first()).toHaveAttribute('aria-expanded', 'true') - await expect(pins.last()).toHaveAttribute('aria-expanded', 'false') - await expect(remoteRow).toHaveCount(0) - }) - } -} diff --git a/tests/e2e/worktree-reveal-host-sections.spec.ts b/tests/e2e/worktree-reveal-host-sections.spec.ts new file mode 100644 index 000000000000..1ad5bc0e6be8 --- /dev/null +++ b/tests/e2e/worktree-reveal-host-sections.spec.ts @@ -0,0 +1,160 @@ +import { expect, test } from './helpers/orca-app' +import { + waitForActiveWorktree, + waitForSessionReady, + waitForStartupWorktreeRefresh +} from './helpers/store' + +const sections = [ + { groupBy: 'none', key: 'pinned', label: 'Pinned', pinned: true }, + { groupBy: 'none', key: 'all', label: 'All', pinned: false }, + { + groupBy: 'workspace-status', + key: 'workspace-status:in-progress', + label: 'In progress', + pinned: false + }, + { groupBy: 'pr-status', key: 'pr:in-progress', label: 'In progress', pinned: false } +] as const + +for (const section of sections) { + for (const hostId of ['ssh:reveal-builder', 'runtime:reveal-builder'] as const) { + for (const collapsedHost of [false, true]) { + test(`reveals ${hostId} ${section.key} workspace without expanding local section (host collapsed: ${collapsedHost})`, async ({ + orcaPage + }, testInfo) => { + await orcaPage.emulateMedia({ reducedMotion: 'reduce' }) + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + await waitForStartupWorktreeRefresh(orcaPage) + const ids = await orcaPage.evaluate( + async ({ hostId, collapsedHost, section }) => { + const store = window.__store! + const state = store.getState() + const sourceRepo = state.repos[0] + const sourceWorktree = state.worktreesByRepo[sourceRepo.id][0] + await state.updateWorktreeMeta(sourceWorktree.id, { + isPinned: section.pinned, + displayName: 'Local workspace' + }) + const local = { + ...sourceWorktree, + hostId: 'local' as const, + isPinned: section.pinned, + displayName: 'Local workspace' + } + const remoteRepo = { + ...sourceRepo, + id: 'reveal-remote-repo', + executionHostId: hostId, + connectionId: hostId.startsWith('ssh:') ? 'reveal-builder' : null, + displayName: 'Remote project' + } + const remote = { + ...sourceWorktree, + id: 'reveal-remote-workspace', + repoId: remoteRepo.id, + hostId, + isPinned: section.pinned, + isMainWorktree: false, + displayName: 'Remote workspace' + } + state.setActiveView('terminal') + state.setSidebarOpen(true) + await window.api.ui.set({ groupBy: section.groupBy }) + state.setShowSleepingWorkspaces(true) + state.setHideDefaultBranchWorkspace(false) + state.setFilterRepoIds([]) + const collapsedGroups = [ + section.key, + `${section.key}:host:${hostId}`, + ...(collapsedHost ? [`host:${hostId}`] : []) + ] + await window.api.ui.set({ collapsedGroups }) + store.setState({ + groupBy: section.groupBy, + repos: [sourceRepo, remoteRepo], + worktreesByRepo: { [sourceRepo.id]: [local], [remoteRepo.id]: [remote] }, + sshTargetLabels: new Map([['reveal-builder', 'Remote builder']]), + sshConnectionStates: new Map([ + [ + 'reveal-builder', + { + targetId: 'reveal-builder', + status: 'connected', + error: null, + reconnectAttempt: 0 + } + ] + ]), + visibleWorkspaceHostIds: ['local', hostId], + workspaceHostScope: 'all', + activeRepoId: remoteRepo.id, + activeWorktreeId: remote.id, + activeWorkspaceKey: `worktree:${remote.id}`, + activeWorkspaceExecutionHostId: hostId, + pendingRevealWorktree: null, + collapsedGroups: new Set(collapsedGroups) + }) + return { local: local.id, remote: remote.id } + }, + { hostId, collapsedHost, section } + ) + const headers = orcaPage.getByRole('button', { name: new RegExp(`^${section.label}`) }) + const localRow = orcaPage.locator( + `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.local)}]` + ) + const remoteRow = orcaPage.locator( + `[data-worktree-sidebar] [role="option"][data-worktree-id=${JSON.stringify(ids.remote)}]` + ) + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.first()).toHaveAttribute('aria-expanded', 'false') + } + await expect(localRow).toHaveCount(0) + await expect(remoteRow).toHaveCount(0) + await orcaPage.screenshot({ + path: testInfo.outputPath('before-reveal.png'), + clip: { x: 0, y: 170, width: 280, height: 500 } + }) + await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() + await expect(remoteRow).toBeVisible() + await expect(remoteRow).toHaveAttribute('data-scroll-reveal-highlight', 'true') + await orcaPage.screenshot({ + path: testInfo.outputPath('after-reveal.png'), + clip: { x: 0, y: 170, width: 280, height: 500 } + }) + await expect(headers).toHaveCount(2) + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.first()).toHaveAttribute('aria-expanded', 'false') + } + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.last()).toHaveAttribute('aria-expanded', 'true') + } + await expect(localRow).toHaveCount(0) + + await headers.last().click() + await expect(remoteRow).toHaveCount(0) + await orcaPage.evaluate((localId) => { + const store = window.__store! + const local = Object.values(store.getState().worktreesByRepo) + .flat() + .find((worktree) => worktree.id === localId)! + store.setState({ + activeRepoId: local.repoId, + activeWorktreeId: localId, + activeWorkspaceKey: `worktree:${localId}`, + activeWorkspaceExecutionHostId: 'local', + pendingRevealWorktree: null + }) + }, ids.local) + await orcaPage.getByRole('button', { name: 'Reveal active workspace' }).click() + await expect(localRow).toBeVisible() + if (section.pinned || section.groupBy === 'workspace-status') { + await expect(headers.first()).toHaveAttribute('aria-expanded', 'true') + await expect(headers.last()).toHaveAttribute('aria-expanded', 'false') + } + await expect(remoteRow).toHaveCount(0) + }) + } + } +} From d72a40d3a25a20c1aeb2f0b0d53c157484efe40b Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 12:25:25 -0700 Subject: [PATCH 2/3] fix: consolidate sidebar reveal group key imports --- .../sidebar/worktree-list/navigation/pending-reveal-inputs.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 0fb42e525d95..c377ce866e98 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 @@ -11,8 +11,7 @@ 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 { PINNED_GROUP_KEY } from '../grouping/group-keys' -import { getWorktreeLineageGroupKey } from '../grouping/group-keys' +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' From 030d26d29ae01e6de363d63e81e76efd8939a970 Mon Sep 17 00:00:00 2001 From: Neil Date: Tue, 6 Oct 2026 12:56:46 -0700 Subject: [PATCH 3/3] test: publish cancellation readiness after signal setup --- .../text-generation/source-control-local-process.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/text-generation/source-control-local-process.test.ts b/src/main/text-generation/source-control-local-process.test.ts index 508df035f086..34c34009048c 100644 --- a/src/main/text-generation/source-control-local-process.test.ts +++ b/src/main/text-generation/source-control-local-process.test.ts @@ -251,13 +251,14 @@ describe.runIf(process.platform !== 'win32')( const slowStop = join(folder, 'slow-stop.cjs') writeFileSync( slowStop, - `require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)) -process.stdin.resume() + `process.stdin.resume() process.on('SIGTERM', () => setTimeout(() => { require('node:fs').writeFileSync(${JSON.stringify(stoppedFile)}, 'stopped') process.exit(0) }, 300)) setInterval(() => {}, 60000) +// Publish readiness only after the signal handler is installed. +require('node:fs').writeFileSync(${JSON.stringify(pidFile)}, String(process.pid)) ` ) const answers = join(folder, 'answers.cjs')