fix(sidebar): make Cmd/Ctrl+1-9 match the rendered card order when the sidebar is closed (#10693)

This commit is contained in:
Neil 2026-07-26 01:51:11 -07:00 committed by GitHub
parent 6c5af95343
commit 03b65d08e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 484 additions and 47 deletions

View File

@ -219,7 +219,6 @@ import {
} from './worktree-multi-selection'
import { persistWorktreeSortOrderByHost } from '@/lib/worktree-sort-order-persistence'
import {
ALL_EXECUTION_HOSTS_SCOPE,
getRepoExecutionHostId,
getSettingsFocusedExecutionHostId,
getWorktreeExecutionHostId,
@ -294,9 +293,10 @@ import {
sidebarWorkspaceStillExists
} from './worktree-list-folder-reveal'
import {
filterFolderWorkspacesForVisibleHosts,
filterProjectGroupsForVisibleHosts,
getFolderPathStatusRouteOptionsForRows,
getFolderWorkspaceExecutionHostIdForRows,
getProjectGroupExecutionHostIdForRows
getVisibleSidebarHostIdSet
} from './worktree-list-host-filtering'
import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display'
import {
@ -5615,12 +5615,10 @@ const WorktreeList = React.memo(function WorktreeList({
worktreeMap
])
const defaultHostId = getSettingsFocusedExecutionHostId(settings)
const visibleHostIdSet = useMemo(() => {
const visibleHostIds =
visibleWorkspaceHostIds ??
(workspaceHostScope === ALL_EXECUTION_HOSTS_SCOPE ? null : [workspaceHostScope])
return visibleHostIds ? new Set<ExecutionHostId>(visibleHostIds) : null
}, [visibleWorkspaceHostIds, workspaceHostScope])
const visibleHostIdSet = useMemo(
() => getVisibleSidebarHostIdSet(visibleWorkspaceHostIds, workspaceHostScope),
[visibleWorkspaceHostIds, workspaceHostScope]
)
const visibleReposForRows = useMemo(() => {
if (!visibleHostIdSet) {
return repos
@ -5631,29 +5629,20 @@ const WorktreeList = React.memo(function WorktreeList({
return visibleHostIdSet.has(hostId)
})
}, [defaultHostId, repos, visibleHostIdSet])
const visibleProjectGroupsForRows = useMemo(() => {
if (!visibleHostIdSet) {
return projectGroups
}
return projectGroups.filter((group) => {
const hostId = getProjectGroupExecutionHostIdForRows(group, defaultHostId)
return visibleHostIdSet.has(hostId)
})
}, [defaultHostId, projectGroups, visibleHostIdSet])
const visibleFolderWorkspacesForRows = useMemo(() => {
if (!visibleHostIdSet) {
return folderWorkspaces
}
const projectGroupById = new Map(projectGroups.map((group) => [group.id, group]))
return folderWorkspaces.filter((folderWorkspace) => {
const hostId = getFolderWorkspaceExecutionHostIdForRows({
folderWorkspace,
projectGroup: projectGroupById.get(folderWorkspace.projectGroupId),
const visibleProjectGroupsForRows = useMemo(
() => filterProjectGroupsForVisibleHosts(projectGroups, visibleHostIdSet, defaultHostId),
[defaultHostId, projectGroups, visibleHostIdSet]
)
const visibleFolderWorkspacesForRows = useMemo(
() =>
filterFolderWorkspacesForVisibleHosts(
folderWorkspaces,
projectGroups,
visibleHostIdSet,
defaultHostId
})
return visibleHostIdSet.has(hostId)
})
}, [defaultHostId, folderWorkspaces, projectGroups, visibleHostIdSet])
),
[defaultHostId, folderWorkspaces, projectGroups, visibleHostIdSet]
)
const repoOrder = useMemo(() => {
return getLogicalRepoOrderRankById(repos.map((repo) => repo.id))
}, [repos])
@ -5974,8 +5963,8 @@ const WorktreeList = React.memo(function WorktreeList({
// Why layout effect: the Cmd/Ctrl+19 handler can fire right after commit; publishing after paint would leave the shortcut cache stale.
useLayoutEffect(() => {
setVisibleWorktreeIds(renderedWorktreeIds)
// Why: unmounting the list clears the rendered-order cache so shortcuts fall back to the live store snapshot.
return () => setVisibleWorktreeIds([])
// Why null, not []: [] is a real rendered order (all collapsed/filtered); null tells shortcuts the list is unmounted.
return () => setVisibleWorktreeIds(null)
}, [renderedWorktreeIds])
const handleCreateForRepo = useCallback(

View File

@ -0,0 +1,262 @@
import { afterEach, describe, expect, it } from 'vitest'
import { useAppStore } from '@/store'
import { getVisibleWorktreeIds, setVisibleWorktreeIds } from './visible-worktrees'
import type { AppState } from '@/store/types'
import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../shared/types'
import { folderWorkspaceKey } from '../../../../shared/workspace-scope'
const initialState = useAppStore.getInitialState()
function makeWorktree(id: string, overrides: Partial<Worktree> = {}): Worktree {
return {
id,
repoId: 'repo1',
path: `/tmp/${id}`,
head: 'abc123',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false,
displayName: id,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
...overrides
} as Worktree
}
function makeMainWorktree(id: string, overrides: Partial<Worktree> = {}): Worktree {
return makeWorktree(id, { isMainWorktree: true, branch: 'refs/heads/main', ...overrides })
}
function makeRepo(id = 'repo1', overrides: Partial<Repo> = {}): Repo {
return {
id,
name: id,
path: `/tmp/${id}`,
defaultBranch: 'main',
connectionId: null,
...overrides
} as unknown as Repo
}
function makeProjectGroup(id: string, overrides: Partial<ProjectGroup> = {}): ProjectGroup {
return {
id,
name: id,
parentPath: `/tmp/${id}`,
connectionId: null,
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1,
...overrides
} as ProjectGroup
}
function makeFolderWorkspace(id: string, projectGroupId: string): FolderWorkspace {
return {
id,
projectGroupId,
name: id,
folderPath: `/tmp/${id}`,
connectionId: null,
linkedTask: null,
comment: '',
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0,
createdAt: 1,
updatedAt: 1
}
}
/**
* Seeds the store with the fields the closed-sidebar order reads, then drops the
* published order so getVisibleWorktreeIds() must recompute it.
*/
function seedStore(worktrees: Worktree[], overrides: Partial<AppState> = {}): void {
useAppStore.setState(
{
...initialState,
repos: [makeRepo()],
worktreesByRepo: { repo1: worktrees },
// Why showSleepingWorkspaces: fixtures have no PTYs, so the activity filter
// would otherwise drop every workspace before ordering runs.
showSleepingWorkspaces: true,
groupBy: 'repo',
sortBy: 'manual',
...overrides
} as AppState,
true
)
setVisibleWorktreeIds(null)
}
describe('closed-sidebar Cmd+1-9 ordering (#9497)', () => {
afterEach(() => {
setVisibleWorktreeIds(null)
useAppStore.setState(initialState, true)
})
it('hoists the repo main worktree first, matching the rendered sidebar', () => {
// Manual sort ranks by sortOrder descending, so the base sort puts the feature
// workspace first; only the repo grouping layer hoists main ahead of it.
const feature = makeWorktree('wt-feature', { sortOrder: 1 })
const main = makeMainWorktree('wt-main', { sortOrder: 0 })
seedStore([feature, main])
const order = getVisibleWorktreeIds()
expect(order).toEqual(['wt-main', 'wt-feature'])
// The pre-fix flat fallback returned membership order and numbered the wrong card.
expect(order).not.toEqual(['wt-feature', 'wt-main'])
})
it('ignores the agent-send collapse override, which only applies to a mounted list', () => {
// Safe to ignore WorktreeList's effectiveCollapsedGroups override only because
// it needs a mounted list, which publishes its order and skips this path.
const main = makeMainWorktree('wt-main')
const feature = makeWorktree('wt-feature')
seedStore([main, feature], {
collapsedGroups: new Set(['project:repo:repo1']),
agentSendPopoverTargetMode: {
worktreeId: 'wt-feature',
id: 'send-1',
instanceId: 'inst-1'
} as AppState['agentSendPopoverTargetMode']
})
expect(getVisibleWorktreeIds()).toEqual([])
setVisibleWorktreeIds(['wt-feature', 'wt-main'])
expect(getVisibleWorktreeIds()).toEqual(['wt-feature', 'wt-main'])
})
it('places a pinned workspace per the pinned section under both display policies', () => {
const main = makeMainWorktree('wt-main')
const plain = makeWorktree('wt-plain')
const pinned = makeWorktree('wt-pinned', { isPinned: true })
for (const showPinnedWorktreesInGroups of [false, true]) {
seedStore([main, plain, pinned], {
settings: { showPinnedWorktreesInGroups } as AppState['settings']
})
const order = getVisibleWorktreeIds()
// Each workspace owns exactly one ordinal even when pinning duplicates its row.
expect(order).toEqual([...new Set(order)])
expect([...order].sort()).toEqual(['wt-main', 'wt-pinned', 'wt-plain'])
expect(order[0]).toBe(showPinnedWorktreesInGroups ? 'wt-main' : 'wt-pinned')
}
})
it('elides members of a collapsed group, which render no card to number', () => {
const main = makeMainWorktree('wt-main')
const feature = makeWorktree('wt-feature')
seedStore([main, feature], { collapsedGroups: new Set(['project:repo:repo1']) })
expect(getVisibleWorktreeIds()).toEqual([])
})
it('numbers folder workspaces, which the flat fallback omitted entirely', () => {
const group = makeProjectGroup('group-1')
const folderWorkspace = makeFolderWorkspace('fw-1', group.id)
seedStore([makeMainWorktree('wt-main')], {
projectGroups: [group],
folderWorkspaces: [folderWorkspace]
})
expect(getVisibleWorktreeIds()).toContain(folderWorkspaceKey(folderWorkspace.id))
})
it('numbers a workspace created while nothing was published', () => {
// A retained-cache fix cannot surface this: it can only prune ids it already had.
seedStore([makeMainWorktree('wt-main')])
expect(getVisibleWorktreeIds()).toEqual(['wt-main'])
seedStore([makeMainWorktree('wt-main'), makeWorktree('wt-created-while-closed')])
expect(getVisibleWorktreeIds()).toEqual(['wt-main', 'wt-created-while-closed'])
})
it('treats a published order as authoritative, including an explicitly empty one', () => {
seedStore([makeMainWorktree('wt-main'), makeWorktree('wt-feature')])
setVisibleWorktreeIds(['x'])
expect(getVisibleWorktreeIds()).toEqual(['x'])
// An empty rendered sidebar is a real order; the old length check fell through here.
setVisibleWorktreeIds([])
expect(getVisibleWorktreeIds()).toEqual([])
setVisibleWorktreeIds(null)
expect(getVisibleWorktreeIds()).toEqual(['wt-main', 'wt-feature'])
})
it('honors an explicit host filter and keeps the all-hosts default unfiltered', () => {
const localRepo = makeRepo('repo1')
const sshRepo = makeRepo('repo-ssh', { connectionId: 'my-target' })
// Base sort interleaves the hosts, so any host grouping has to reorder them.
const localMain = makeMainWorktree('wt-local-main', { sortOrder: 3 })
const remoteMain = makeMainWorktree('wt-remote-main', { repoId: 'repo-ssh', sortOrder: 2 })
const localFeature = makeWorktree('wt-local-feature', { sortOrder: 1 })
const remoteFeature = makeWorktree('wt-remote-feature', { repoId: 'repo-ssh', sortOrder: 0 })
const storeOverrides = {
repos: [localRepo, sshRepo],
worktreesByRepo: {
repo1: [localMain, localFeature],
'repo-ssh': [remoteMain, remoteFeature]
},
sshTargetLabels: new Map([['my-target', 'My Target']])
} as Partial<AppState>
seedStore([], storeOverrides)
// Default all-hosts scope adds no host sections, so repo grouping alone orders it.
expect(getVisibleWorktreeIds()).toEqual([
'wt-local-main',
'wt-local-feature',
'wt-remote-main',
'wt-remote-feature'
])
seedStore([], { ...storeOverrides, visibleWorkspaceHostIds: ['local'] })
expect(getVisibleWorktreeIds()).toEqual(['wt-local-main', 'wt-local-feature'])
seedStore([], {
...storeOverrides,
visibleWorkspaceHostIds: ['ssh:my-target', 'local']
})
// Host sections keep each host's workspaces contiguous, ordered by the host
// registry (local first) rather than by the filter argument's order.
expect(getVisibleWorktreeIds()).toEqual([
'wt-local-main',
'wt-local-feature',
'wt-remote-main',
'wt-remote-feature'
])
})
it('keeps the sort layer intact for smart and comparator sort modes', () => {
const main = makeMainWorktree('wt-main')
const older = makeWorktree('wt-older', { displayName: 'b-older', lastActivityAt: 1 })
const newer = makeWorktree('wt-newer', { displayName: 'a-newer', lastActivityAt: 999 })
seedStore([older, newer, main], { sortBy: 'smart' })
expect(getVisibleWorktreeIds()).toEqual(['wt-main', 'wt-newer', 'wt-older'])
seedStore([older, newer, main], { sortBy: 'name' })
expect(getVisibleWorktreeIds()).toEqual(['wt-main', 'wt-newer', 'wt-older'])
})
})

View File

@ -0,0 +1,122 @@
import type { Worktree } from '../../../../shared/types'
import type { AppState } from '@/store/types'
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
import {
ALL_EXECUTION_HOSTS_SCOPE,
getSettingsFocusedExecutionHostId
} from '../../../../shared/execution-host'
import { getRepoMapFromState, getWorktreeMapFromState } from '@/store/selectors'
import { getProjectHostSetupProjectionFromState } from '@/store/project-host-setup-selector'
import { buildRows, getPinnedWorktreeDisplayPolicy } from './worktree-list-groups'
import { addHostSectionRows } from './host-section-rows'
import { orderHostSectionOptions } from './host-section-order'
import { buildSidebarHostOptions } from './sidebar-host-options'
import { getLogicalRepoOrderRankById } from './project-header-drop'
import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference'
import { selectWorktreeListReviewCacheInputs } from './worktree-list-review-cache-inputs'
import {
filterFolderWorkspacesForVisibleHosts,
filterProjectGroupsForVisibleHosts,
getVisibleSidebarHostIdSet
} from './worktree-list-host-filtering'
const EMPTY_REPO_ID_SET: ReadonlySet<string> = Object.freeze(new Set<string>())
const EMPTY_IMPORTED_BY_REPO = Object.freeze(new Map()) as never
const EMPTY_INBOX_BY_REPO = Object.freeze(new Map()) as never
const EMPTY_PENDING_CREATIONS = Object.freeze([]) as never
/**
* Orders already-filtered worktrees the way the sidebar would render them, for
* Cmd+19 numbering while WorktreeList is unmounted (#9497).
*
* Why replay the pipeline: a flat list drops grouping, pinning, main-worktree
* hoisting and collapse elision, so the shortcut numbered the wrong card.
* Why worktrees are passed in: keeps the dependency on visible-worktrees.ts
* one-way, since test suites mock that module's path.
*/
export function computeRenderedSidebarWorktreeOrder(
state: AppState,
visibleWorktrees: readonly Worktree[]
): string[] {
const defaultHostId = getSettingsFocusedExecutionHostId(state.settings)
const pinnedDisplayPolicy = getPinnedWorktreeDisplayPolicy(state.settings)
const projection = getProjectHostSetupProjectionFromState(state)
const visibleHostIdSet = getVisibleSidebarHostIdSet(
state.visibleWorkspaceHostIds,
state.workspaceHostScope
)
const projectGroups = state.projectGroups ?? []
const { prCache } = selectWorktreeListReviewCacheInputs(
state,
state.groupBy,
state.worktreeCardProperties
)
const rows = buildRows(
state.groupBy,
[...visibleWorktrees],
getRepoMapFromState(state),
prCache,
state.collapsedGroups,
getLogicalRepoOrderRankById(state.repos.map((repo) => repo.id)),
state.workspaceStatuses,
state.projectOrderBy,
state.worktreeLineageById,
getWorktreeMapFromState(state),
true,
state.settings,
filterProjectGroupsForVisibleHosts(projectGroups, visibleHostIdSet, defaultHostId),
// Why empty: placeholder/imported/inbox/pending inputs never emit item or folder-workspace rows, the only two the order reads.
EMPTY_REPO_ID_SET,
EMPTY_IMPORTED_BY_REPO,
EMPTY_INBOX_BY_REPO,
EMPTY_PENDING_CREATIONS,
{ projects: projection.projects, projectHostSetups: projection.setups },
filterFolderWorkspacesForVisibleHosts(
state.folderWorkspaces,
projectGroups,
visibleHostIdSet,
defaultHostId
),
// Why no hostLabelById: it only feeds display-only host context labels, never row order.
undefined,
defaultHostId,
pinnedDisplayPolicy
)
// Why lazy: with no host filter, addHostSectionRows is a pass-through, so skip building the whole host registry on a keystroke.
// Deliberately a superset of its internal guards — on <=1 host it still no-ops, wasting only the registry build.
const needsHostSections =
state.workspaceHostScope !== ALL_EXECUTION_HOSTS_SCOPE || state.visibleWorkspaceHostIds != null
const sectionRows = needsHostSections
? addHostSectionRows({
rows,
hostOptions: orderHostSectionOptions(
buildSidebarHostOptions({
repos: state.repos,
sshTargetLabels: state.sshTargetLabels,
sshConnectionStates: state.sshConnectionStates,
settings: state.settings,
runtimeEnvironments: state.runtimeEnvironments,
runtimeStatusByEnvironmentId: state.runtimeStatusByEnvironmentId,
hostLabelOverrides: getHostDisplayLabelOverrides(state.settings)
}),
state.workspaceHostOrder
),
workspaceHostScope: state.workspaceHostScope,
visibleWorkspaceHostIds: state.visibleWorkspaceHostIds,
defaultHostId,
collapsedHostKeys: state.collapsedGroups,
forceCollapseHosts: false,
preferProjectGrouping: true
})
: rows
return Array.from(
new Set(
getRenderedWorktreesInSidebarOrder(sectionRows, pinnedDisplayPolicy).map(
(worktree) => worktree.id
)
)
)
}

View File

@ -2,7 +2,11 @@ import type { Worktree, Repo, TerminalTab, WorktreeLineage } from '../../../../s
import { buildWorktreeComparator, sortWorktreesSmart } from './smart-sort'
import { getWorktreeIdsWithLiveAgent, isInactiveWorkspace } from '@/lib/worktree-activity-state'
import { useAppStore } from '@/store'
import { getAllWorktreesFromState, getRepoMapFromState } from '@/store/selectors'
import {
getAllWorktreesFromState,
getRepoMapFromState,
getWorktreeMapFromState
} from '@/store/selectors'
import { DEFAULT_SHOW_SLEEPING_WORKSPACES } from '../../../../shared/constants'
import {
ALL_EXECUTION_HOSTS_SCOPE,
@ -15,6 +19,7 @@ import {
getCyclicProjectedWorktreeLineageIds,
getLineageRenderInfo
} from './worktree-lineage-projection'
import { computeRenderedSidebarWorktreeOrder } from './rendered-sidebar-worktree-order'
/**
* Whether a worktree represents the repo's default-branch row that the
@ -270,15 +275,19 @@ function addVisibleLineageAncestors(
* could target a different worktree than what's rendered at that sidebar
* position. By caching the IDs that WorktreeList actually rendered, the
* shortcut numbering always matches the sidebar card order.
*
* Why null vs []: [] is a real rendered order (everything collapsed/filtered);
* null means WorktreeList is unmounted.
*/
let _cachedVisibleIds: string[] = []
let _publishedVisibleIds: string[] | null = null
/**
* Called by WorktreeList after computing visible worktrees so the Cmd+19
* handler can read the exact same ordering the user sees on screen.
* handler can read the exact same ordering the user sees on screen. Pass null
* on unmount.
*/
export function setVisibleWorktreeIds(ids: string[]): void {
_cachedVisibleIds = ids
export function setVisibleWorktreeIds(ids: string[] | null): void {
_publishedVisibleIds = ids
}
/**
@ -286,17 +295,16 @@ export function setVisibleWorktreeIds(ids: string[]): void {
* state. Called by the App-level Cmd+19 handler (not a React hook reads
* store snapshot at call time).
*
* If WorktreeList has rendered at least once, returns the cached IDs so the
* shortcut numbering matches the sidebar. Falls back to a live recomputation
* only before WorktreeList's first render (e.g. app startup).
* If WorktreeList is mounted, returns the exact IDs it rendered. Otherwise
* recomputes the order the sidebar *would* render from the same row pipeline,
* so a closed sidebar numbers workspaces the same way an open one does (#9497).
*/
export function getVisibleWorktreeIds(): string[] {
// Prefer the cached IDs that mirror the rendered sidebar order.
if (_cachedVisibleIds.length > 0) {
return _cachedVisibleIds
// Prefer the published IDs that mirror the rendered sidebar order.
if (_publishedVisibleIds) {
return _publishedVisibleIds
}
// Fallback: live recomputation for the window before WorktreeList renders.
const state = useAppStore.getState()
const allWorktrees = getAllWorktreesFromState(state).filter((w) => !w.isArchived)
@ -325,7 +333,7 @@ export function getVisibleWorktreeIds(): string[] {
sortedIds = sorted.map((w) => w.id)
}
return computeVisibleWorktreeIds(state.worktreesByRepo, sortedIds, {
const visibleIds = computeVisibleWorktreeIds(state.worktreesByRepo, sortedIds, {
filterRepoIds: state.filterRepoIds,
showSleepingWorkspaces: state.showSleepingWorkspaces,
tabsByWorktree: state.tabsByWorktree,
@ -345,4 +353,11 @@ export function getVisibleWorktreeIds(): string[] {
defaultHostId: getSettingsFocusedExecutionHostId(state.settings),
worktreeLineageById: state.worktreeLineageById
})
const worktreeMap = getWorktreeMapFromState(state)
// Why the row pipeline: grouping, pinning and main-worktree hoisting reorder cards, so a flat sort numbers the wrong workspace.
return computeRenderedSidebarWorktreeOrder(
state,
visibleIds.map((id) => worktreeMap.get(id)).filter((w): w is Worktree => w != null)
)
}

View File

@ -1,12 +1,61 @@
import {
ALL_EXECUTION_HOSTS_SCOPE,
normalizeExecutionHostId,
parseExecutionHostId,
toSshExecutionHostId,
type ExecutionHostId
type ExecutionHostId,
type ExecutionHostScope
} from '../../../../shared/execution-host'
import type { FolderWorkspacePathStatusRequest } from '../../../../shared/folder-workspace-path-status'
import type { FolderWorkspace, ProjectGroup } from '../../../../shared/types'
/** null means "no host filter" — every host is visible. */
export function getVisibleSidebarHostIdSet(
visibleWorkspaceHostIds: readonly ExecutionHostId[] | null | undefined,
workspaceHostScope: ExecutionHostScope
): Set<ExecutionHostId> | null {
const visibleHostIds =
visibleWorkspaceHostIds ??
(workspaceHostScope === ALL_EXECUTION_HOSTS_SCOPE ? null : [workspaceHostScope])
return visibleHostIds ? new Set<ExecutionHostId>(visibleHostIds) : null
}
// Why shared: the sidebar render path and the Cmd+19 order must apply the same
// host filtering, or the numbering drifts from the cards whenever a filter is on.
export function filterProjectGroupsForVisibleHosts(
projectGroups: readonly ProjectGroup[],
visibleHostIdSet: ReadonlySet<ExecutionHostId> | null,
defaultHostId: ExecutionHostId
): readonly ProjectGroup[] {
if (!visibleHostIdSet) {
return projectGroups
}
return projectGroups.filter((group) =>
visibleHostIdSet.has(getProjectGroupExecutionHostIdForRows(group, defaultHostId))
)
}
export function filterFolderWorkspacesForVisibleHosts(
folderWorkspaces: readonly FolderWorkspace[],
projectGroups: readonly ProjectGroup[],
visibleHostIdSet: ReadonlySet<ExecutionHostId> | null,
defaultHostId: ExecutionHostId
): readonly FolderWorkspace[] {
if (!visibleHostIdSet) {
return folderWorkspaces
}
const projectGroupById = new Map(projectGroups.map((group) => [group.id, group]))
return folderWorkspaces.filter((folderWorkspace) =>
visibleHostIdSet.has(
getFolderWorkspaceExecutionHostIdForRows({
folderWorkspace,
projectGroup: projectGroupById.get(folderWorkspace.projectGroupId),
defaultHostId
})
)
)
}
export function getProjectGroupExecutionHostIdForRows(
group: Pick<ProjectGroup, 'connectionId' | 'executionHostId'>,
defaultHostId: ExecutionHostId