Fix pinned workspace reveal expanding other hosts (#25836)

This commit is contained in:
Neil
2026-10-06 02:25:36 -07:00
committed by GitHub
parent 4de9f9f85a
commit d9cf07d3f4
10 changed files with 329 additions and 32 deletions
@@ -123,6 +123,8 @@ const WorktreeList = React.memo(function WorktreeList({
agentSendTargetWorktreeId
})
const effectiveCollapsedGroups = useEffectiveCollapsedGroups({
hostScopedPinnedGroups:
filterState.workspaceHostScope !== 'all' || filterState.visibleWorkspaceHostIds !== null,
collapsedGroups,
agentSendTargetWorktreeId,
groupBy,
@@ -0,0 +1,84 @@
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}`
])
})
})
}
@@ -0,0 +1,31 @@
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<string>
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]
})
}
@@ -16,6 +16,7 @@ export function getPinnedWorktreeDisplayPolicy(
export type GroupHeaderRow = {
type: 'header'
collapseKey?: string
key: string
label: string
count: number
@@ -11,11 +11,14 @@ 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 { getHostPinnedGroupKey } from '../../host-pinned-sections'
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
collapsedGroups: Set<string>
agentSendTargetWorktreeId: string | null
groupBy: WorktreeGroupBy
@@ -34,6 +37,7 @@ export function useEffectiveCollapsedGroups(args: {
}): Set<string> {
const {
collapsedGroups,
hostScopedPinnedGroups = false,
agentSendTargetWorktreeId,
groupBy,
pinnedDisplayPolicy,
@@ -77,7 +81,17 @@ export function useEffectiveCollapsedGroups(args: {
pinnedDisplayPolicy === 'single-location' &&
isPinnedSectionWorktree(targetWorktree, visibleWorktrees, worktreeLineageById, worktreeMap)
) {
next.delete(PINNED_GROUP_KEY)
next.delete(
hostScopedPinnedGroups
? getHostPinnedGroupKey(
getWorktreeExecutionHostId(
targetWorktree,
repoMap.get(targetWorktree.repoId),
defaultHostId
)
)
: PINNED_GROUP_KEY
)
} else {
for (const groupKey of getGroupKeysForWorktree(
groupBy,
@@ -104,6 +118,7 @@ export function useEffectiveCollapsedGroups(args: {
}, [
agentSendTargetWorktreeId,
collapsedGroups,
hostScopedPinnedGroups,
groupBy,
pinnedDisplayPolicy,
visibleWorktrees,
@@ -16,6 +16,8 @@ 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 { addHostSectionRows } from '../../host-section-rows'
import { orderHostSectionOptions } from '../../host-section-order'
import { buildSidebarHostOptions } from '../../sidebar-host-options'
@@ -141,6 +143,18 @@ export function useSidebarSectionRows(args: SectionRowsArgs) {
[hostOptions]
)
const hostScopedPinnedGroups =
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])
const rows: Row[] = useMemo(
() =>
buildRows(
@@ -148,7 +162,7 @@ export function useSidebarSectionRows(args: SectionRowsArgs) {
worktrees,
repoMap,
args.prCache,
effectiveCollapsedGroups,
rowCollapsedGroups,
repoOrder,
args.workspaceStatuses,
args.projectOrderBy,
@@ -172,7 +186,7 @@ export function useSidebarSectionRows(args: SectionRowsArgs) {
worktrees,
repoMap,
args.prCache,
effectiveCollapsedGroups,
rowCollapsedGroups,
defaultHostId,
repoOrder,
args.workspaceStatuses,
@@ -215,29 +229,35 @@ export function useSidebarSectionRows(args: SectionRowsArgs) {
},
[orderedHostOptions, setWorkspaceHostOrder, workspaceHostOrder]
)
const sectionRows = useMemo(
() =>
addHostSectionRows({
rows,
hostOptions: orderedHostOptions,
workspaceHostScope: args.workspaceHostScope,
visibleWorkspaceHostIds: args.visibleWorkspaceHostIds,
defaultHostId,
collapsedHostKeys: effectiveCollapsedGroups,
forceCollapseHosts: hostDragActive,
// Why: projects/workspaces are the primary sidebar object; host sections are only an explicit host-filter view.
preferProjectGrouping: true
}),
[
args.visibleWorkspaceHostIds,
args.workspaceHostScope,
const sectionRows = useMemo(() => {
const sectioned = addHostSectionRows({
rows,
hostOptions: orderedHostOptions,
workspaceHostScope: args.workspaceHostScope,
visibleWorkspaceHostIds: args.visibleWorkspaceHostIds,
defaultHostId,
effectiveCollapsedGroups,
hostDragActive,
orderedHostOptions,
rows
]
)
collapsedHostKeys: effectiveCollapsedGroups,
forceCollapseHosts: hostDragActive,
// Why: projects/workspaces are the primary sidebar object; host sections are only an explicit host-filter view.
preferProjectGrouping: true
})
return hostScopedPinnedGroups
? scopePinnedSectionCollapse({
rows: sectioned,
collapsedGroups: effectiveCollapsedGroups,
defaultHostId
})
: sectioned
}, [
args.visibleWorkspaceHostIds,
args.workspaceHostScope,
defaultHostId,
effectiveCollapsedGroups,
hostDragActive,
hostScopedPinnedGroups,
orderedHostOptions,
rows
])
const renderedSidebarRowKeys = useMemo(
() => collectRenderedSidebarRowKeys(sectionRows),
[sectionRows]
@@ -10,6 +10,7 @@ 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 { getWorktreeLineageGroupKey } from '../grouping/group-keys'
import type { ProjectGroupingModel } from '../grouping/project-grouping'
import type { PinnedWorktreeDisplayPolicy, WorktreeGroupBy } from '../grouping/row-types'
@@ -87,7 +88,8 @@ export function expandGroupsForWorktreeReveal(
return
}
const targetRepo = args.repoMap.get(targetWorktree.repoId)
const hostGroupKey = `host:${getWorktreeExecutionHostId(targetWorktree, targetRepo, args.defaultHostId)}`
const targetHostId = getWorktreeExecutionHostId(targetWorktree, targetRepo, args.defaultHostId)
const hostGroupKey = `host:${targetHostId}`
if (args.collapsedGroups.has(hostGroupKey)) {
args.toggleGroup(hostGroupKey)
}
@@ -118,6 +120,12 @@ export function expandGroupsForWorktreeReveal(
)
? getPinnedWorktreeRevealCollapsedGroupKeys({
worktree: targetWorktree,
groupKey: args.renderRows.some(
(row) =>
row.type === 'host-header' || (row.type === 'header' && row.collapseKey !== undefined)
)
? getHostPinnedGroupKey(targetHostId)
: undefined,
collapsedGroups: args.collapsedGroups,
inPinnedSection: true
})
@@ -93,10 +93,12 @@ export function getSidebarRowRevealAncestorKeys(args: {
export function getPinnedWorktreeRevealCollapsedGroupKeys({
worktree,
collapsedGroups,
groupKey = PINNED_GROUP_KEY,
inPinnedSection = worktree.isPinned
}: {
worktree: Worktree
collapsedGroups: ReadonlySet<string>
groupKey?: string
inPinnedSection?: boolean
}): string[] {
if (!inPinnedSection) {
@@ -104,8 +106,8 @@ export function getPinnedWorktreeRevealCollapsedGroupKeys({
}
const keys: string[] = []
// Why: the reveal effect already opens this host; re-returning it would toggle it back closed.
if (collapsedGroups.has(PINNED_GROUP_KEY)) {
keys.push(PINNED_GROUP_KEY)
if (collapsedGroups.has(groupKey)) {
keys.push(groupKey)
}
return keys
}
@@ -171,7 +171,8 @@ export function renderWorktreeSectionHeaderRow(args: {
projectGroupId: folderBackedProjectGroup.id
})
: null
const isHeaderCollapsed = ctx.collapsedGroups.has(row.key)
const collapseKey = row.collapseKey ?? row.key
const isHeaderCollapsed = ctx.collapsedGroups.has(collapseKey)
// Why: repo/project/status/pinned share compact section chrome; flat "All" stays a simple label.
const showHeaderCollapseAffordance =
row.count > 0 &&
@@ -284,7 +285,7 @@ export function renderWorktreeSectionHeaderRow(args: {
if (shouldIgnoreRepoHeaderToggle(event)) {
return
}
ctx.toggleGroupWithScrollAnchor(row.key)
ctx.toggleGroupWithScrollAnchor(collapseKey)
}}
onKeyDown={(e) => {
if (shouldIgnoreRepoHeaderToggle(e)) {
@@ -292,7 +293,7 @@ export function renderWorktreeSectionHeaderRow(args: {
}
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
ctx.toggleGroupWithScrollAnchor(row.key)
ctx.toggleGroupWithScrollAnchor(collapseKey)
}
}}
>
@@ -350,7 +351,7 @@ export function renderWorktreeSectionHeaderRow(args: {
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
ctx.toggleGroupWithScrollAnchor(row.key)
ctx.toggleGroupWithScrollAnchor(collapseKey)
}}
>
<ChevronDown
@@ -0,0 +1,133 @@
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)
})
}
}