diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 1c9f07daaf8..c084632d8e9 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -34,7 +34,6 @@ import { useWorktreeMap } from '@/store/selectors' import WorktreeCard, { type ActiveSurfaceVariant } from './WorktreeCard' -import StatusIndicator from './StatusIndicator' import { WorktreeSidebarDropIndicator } from './WorktreeSidebarDropIndicator' import { getProjectGroupHeaderSectionEndByGroupId, @@ -125,11 +124,8 @@ import { getWorktreeIdsWithLiveAgent } from '@/lib/worktree-activity-state' import { getEmptyProjectPlaceholderRepoIds } from './empty-project-placeholder-repos' import { getVisibleWorktreeBrowserActivityTabs, - getVisibleWorktreeTerminalActivityTabs, - getWorktreeSectionTerminalActivityTabs + getVisibleWorktreeTerminalActivityTabs } from './visible-worktree-activity-inputs' -import { selectTerminalLayoutRootsForWorktrees } from './worktree-card-status-inputs' -import { buildWorktreeSectionActivitySummaries } from './worktree-section-activity' import { selectWorktreeListReviewCacheInputs } from './worktree-list-review-cache-inputs' import { VIRTUALIZED_SCROLL_ANCHOR_RECORD_EVENT, @@ -295,6 +291,10 @@ import { getProjectGroupExecutionHostIdForRows } from './worktree-list-host-filtering' import { getFolderWorkspaceCardPrDisplay } from './folder-workspace-card-pr-display' +import { + getPreferredWorktreeRows, + getRenderedWorktreesInSidebarOrder +} from './worktree-sidebar-row-preference' export { getScrollTopToRevealBounds, @@ -743,27 +743,6 @@ function SectionMetricsBadge({ count }: { count: number }): React.JSX.Element { ) } -function SectionRunningBadge({ count }: { count: number }): React.JSX.Element { - const label = formatSectionActivityLabel(count, 'running workspace') - - return ( - - - - - - - {label} - - - ) -} - function HostHeaderHealthIcon({ health }: { @@ -1090,41 +1069,6 @@ function getRenderRowWorktreeItem(row: RenderRow, worktreeId: string): WorktreeI return row.type === 'item' && row.worktree.id === worktreeId ? row : null } -function getPreferredWorktreeRows( - rows: readonly WorktreeItemRow[], - pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy -): WorktreeItemRow[] { - // Why: when pinned rows are duplicated into natural groups, navigation and - // selection should prefer the natural-group copy and only fall back to pinned. - if (pinnedDisplayPolicy === 'single-location') { - const seen = new Set() - return rows.filter((row) => { - if (seen.has(row.worktree.id)) { - return false - } - seen.add(row.worktree.id) - return true - }) - } - const preferredRows: WorktreeItemRow[] = [] - const seen = new Set() - for (const row of rows) { - if (isPinnedWorktreeRow(row) || seen.has(row.worktree.id)) { - continue - } - preferredRows.push(row) - seen.add(row.worktree.id) - } - for (const row of rows) { - if (seen.has(row.worktree.id)) { - continue - } - preferredRows.push(row) - seen.add(row.worktree.id) - } - return preferredRows -} - function getRenderRowOptionId( row: RenderRow | undefined, worktreeId?: string | null @@ -4457,9 +4401,6 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp
{row.label}
- {isHeaderCollapsed && row.runningCount && row.runningCount > 0 ? ( - - ) : null} @@ -5415,20 +5356,6 @@ const WorktreeList = React.memo(function WorktreeList({ ) const settings = useAppStore((s) => s.settings) const pinnedDisplayPolicy = getPinnedWorktreeDisplayPolicy(settings) - const sectionActivitySourceState = useAppStore( - useShallow((s) => ({ - tabsByWorktree: getWorktreeSectionTerminalActivityTabs(s.tabsByWorktree), - browserTabsByWorktree: getVisibleWorktreeBrowserActivityTabs(s.browserTabsByWorktree), - ptyIdsByTabId: s.ptyIdsByTabId, - runtimePaneTitlesByTabId: s.runtimePaneTitlesByTabId, - terminalLayoutsByTabId: s.terminalLayoutsByTabId, - agentStatusEpoch: s.agentStatusEpoch, - agentStatusByPaneKey: s.agentStatusByPaneKey, - migrationUnsupportedByPtyId: s.migrationUnsupportedByPtyId, - retainedAgentsByPaneKey: s.retainedAgentsByPaneKey, - runtimeAgentOrchestrationByPaneKey: s.runtimeAgentOrchestrationByPaneKey - })) - ) const sshTargetLabels = useAppStore((s) => s.sshTargetLabels) const sshConnectionStates = useAppStore((s) => s.sshConnectionStates) const runtimeEnvironments = useAppStore((s) => s.runtimeEnvironments) @@ -5929,49 +5856,6 @@ const WorktreeList = React.memo(function WorktreeList({ () => new Map(hostOptions.map((host) => [host.id, host.label])), [hostOptions] ) - const sectionActivityWorktreeIds = useMemo( - () => worktrees.map((worktree) => worktree.id), - [worktrees] - ) - const sectionActivityState = useMemo( - () => ({ - ...sectionActivitySourceState, - terminalLayoutRootsByTabId: selectTerminalLayoutRootsForWorktrees( - sectionActivitySourceState, - sectionActivityWorktreeIds - ) - }), - [sectionActivitySourceState, sectionActivityWorktreeIds] - ) - const sectionActivitySummaries = useMemo( - () => - buildWorktreeSectionActivitySummaries({ - groupBy, - worktrees, - repoMap, - prCache, - workspaceStatuses, - settings, - projectGroups: visibleProjectGroupsForRows, - projectGrouping, - pinnedDisplayPolicy, - defaultHostId, - state: sectionActivityState - }), - [ - defaultHostId, - groupBy, - pinnedDisplayPolicy, - prCache, - projectGrouping, - repoMap, - sectionActivityState, - settings, - visibleProjectGroupsForRows, - workspaceStatuses, - worktrees - ] - ) // Build flat row list for rendering const rows: Row[] = useMemo( @@ -5998,8 +5882,7 @@ const WorktreeList = React.memo(function WorktreeList({ visibleFolderWorkspacesForRows, hostLabelById, defaultHostId, - pinnedDisplayPolicy, - sectionActivitySummaries + pinnedDisplayPolicy ), [ groupBy, @@ -6022,8 +5905,7 @@ const WorktreeList = React.memo(function WorktreeList({ newExternalWorktreesInboxByRepo, pendingCreations, hostLabelById, - pinnedDisplayPolicy, - sectionActivitySummaries + pinnedDisplayPolicy ] ) const orderedHostOptions = useMemo( @@ -6105,23 +5987,10 @@ const WorktreeList = React.memo(function WorktreeList({ // 'pr-status') can reorder cards into grouped sections. Using the flat // order would cause Cmd+1–9 shortcuts to not match the visual card // positions when grouping is active. - const renderedWorktrees = useMemo(() => { - const itemRows: WorktreeItemRow[] = [] - const folderRows: Worktree[] = [] - for (const row of sectionRows) { - if (row.type === 'item') { - itemRows.push(row) - continue - } - if (row.type === 'folder-workspace') { - folderRows.push(folderWorkspaceToWorktree(row.folderWorkspace)) - } - } - return [ - ...getPreferredWorktreeRows(itemRows, pinnedDisplayPolicy).map((row) => row.worktree), - ...folderRows - ] - }, [pinnedDisplayPolicy, sectionRows]) + const renderedWorktrees = useMemo( + () => getRenderedWorktreesInSidebarOrder(sectionRows, pinnedDisplayPolicy), + [pinnedDisplayPolicy, sectionRows] + ) const renderedWorktreeIds = useMemo( () => uniqueWorktreeIds(renderedWorktrees.map((worktree) => worktree.id)), [renderedWorktrees] diff --git a/src/renderer/src/components/sidebar/host-section-rows.test.ts b/src/renderer/src/components/sidebar/host-section-rows.test.ts index 136b835eb33..dfd77219b12 100644 --- a/src/renderer/src/components/sidebar/host-section-rows.test.ts +++ b/src/renderer/src/components/sidebar/host-section-rows.test.ts @@ -444,9 +444,7 @@ describe('addHostSectionRows', () => { count: 1, hostWorktreeCounts: new Map([[localHostId, 1]]), hostWorktreeIds: new Map([[localHostId, ['local-wt']]]), - worktreeIds: ['local-wt'], - runningCount: 1, - hostRunningCounts: new Map([[localHostId, 1]]) + worktreeIds: ['local-wt'] }, repoHeader(ssh), item('ssh-wt', ssh) @@ -480,7 +478,7 @@ describe('addHostSectionRows', () => { { hostId: 'ssh:ssh-1', count: 1 } ]) expect(sectioned.filter((row) => row.type === 'header' && row.key === 'all')).toMatchObject([ - { hostId: 'local', count: 1, runningCount: 1, worktreeIds: ['local-wt'] } + { hostId: 'local', count: 1, worktreeIds: ['local-wt'] } ]) }) diff --git a/src/renderer/src/components/sidebar/host-section-rows.ts b/src/renderer/src/components/sidebar/host-section-rows.ts index f20e7e8d2cd..0a706699095 100644 --- a/src/renderer/src/components/sidebar/host-section-rows.ts +++ b/src/renderer/src/components/sidebar/host-section-rows.ts @@ -146,13 +146,11 @@ function localizePendingRowsForHost( } const count = row.hostWorktreeCounts.get(hostId) if (count !== undefined && count > 0) { - const runningCount = row.hostRunningCounts?.get(hostId) localized.push({ ...row, count, hostId, - worktreeIds: row.hostWorktreeIds?.get(hostId) ?? row.worktreeIds, - runningCount: runningCount && runningCount > 0 ? runningCount : undefined + worktreeIds: row.hostWorktreeIds?.get(hostId) ?? row.worktreeIds }) } } @@ -219,13 +217,11 @@ export function addHostSectionRows(args: { } const hostRows = rowsByHostId.get(hostId) ?? [] const hostIds = row.hostWorktreeIds?.get(hostId) - const runningCount = row.hostRunningCounts?.get(hostId) hostRows.push({ ...row, count, hostId, - worktreeIds: hostIds ?? row.worktreeIds, - runningCount: runningCount && runningCount > 0 ? runningCount : undefined + worktreeIds: hostIds ?? row.worktreeIds }) rowsByHostId.set(hostId, hostRows) } diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts index fe2e2dd9524..b96357dc774 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts @@ -2176,64 +2176,6 @@ describe('project groups', () => { }) }) - it('attaches activity summaries to Project Group headers', () => { - const group: ProjectGroup = { - id: 'group-1', - name: 'Platform', - parentPath: '/platform', - parentGroupId: null, - createdFrom: 'folder-scan', - tabOrder: 0, - isCollapsed: false, - color: null, - createdAt: 1, - updatedAt: 1 - } - const groupedRepo: Repo = { ...repo, projectGroupId: group.id } - const hostRunningCounts = new Map([['local', 1] as const]) - - const rows = buildRows( - 'repo', - [], - new Map([[groupedRepo.id, groupedRepo]]), - null, - new Set(), - undefined, - undefined, - undefined, - {}, - new Map(), - false, - undefined, - [group], - new Set([groupedRepo.id]), - new Map(), - new Map(), - [], - undefined, - [], - undefined, - 'local', - 'single-location', - new Map([ - [ - 'project-group:group-1', - { - runningCount: 1, - hostRunningCounts - } - ] - ]) - ) - - expect(rows[0]).toMatchObject({ - type: 'header', - key: 'project-group:group-1', - runningCount: 1, - hostRunningCounts - }) - }) - it('does not resurrect filtered repos as empty Project Group headers', () => { const group: ProjectGroup = { id: 'group-1', diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.ts b/src/renderer/src/components/sidebar/worktree-list-groups.ts index 73b013fdb79..7a61827cce8 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.ts @@ -34,7 +34,6 @@ import type { AppState } from '../../store/types' import { getGitHubPRCacheKey, getLegacyGitHubPRCacheKey } from '../../store/slices/github-cache-key' import { getRepoDisplayLabelKey, getRepoDisplayLabelsByPath } from '@/lib/repo-display-labels' import { translate } from '@/i18n/i18n' -import type { WorktreeSectionActivitySummary } from './worktree-section-activity' import { getExecutionHostLabel, LOCAL_EXECUTION_HOST_ID, @@ -69,8 +68,6 @@ export type GroupHeaderRow = { hostWorktreeCounts?: ReadonlyMap hostWorktreeIds?: ReadonlyMap worktreeIds?: readonly string[] - runningCount?: number - hostRunningCounts?: ReadonlyMap } export type WorktreeRow = { @@ -468,8 +465,7 @@ function emitPinnedGroup( renderedNaturalAnchorRepoIds: ReadonlySet, importedWorktreesByRepo: ReadonlyMap, allowImportedFallback: boolean, - result: Row[], - activitySummary?: WorktreeSectionActivitySummary + result: Row[] ): void { const pinned = worktrees.filter((w) => w.isPinned) if (pinned.length === 0) { @@ -500,9 +496,7 @@ function emitPinnedGroup( icon: PINNED_GROUP_META.icon, hostWorktreeCounts, hostWorktreeIds, - worktreeIds: pinned.map((worktree) => worktree.id), - runningCount: activitySummary?.runningCount, - hostRunningCounts: activitySummary?.hostRunningCounts + worktreeIds: pinned.map((worktree) => worktree.id) }) if (collapsedGroups.has(PINNED_GROUP_KEY)) { for (const repoId of pinnedRepoOrder) { @@ -1014,8 +1008,7 @@ export function buildRows( folderWorkspaces: readonly FolderWorkspace[] = [], hostLabelById?: ReadonlyMap, defaultHostId: ExecutionHostId = LOCAL_EXECUTION_HOST_ID, - pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy = getPinnedWorktreeDisplayPolicy(settings), - sectionActivitySummaries?: ReadonlyMap + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy = getPinnedWorktreeDisplayPolicy(settings) ): Row[] { const result: Row[] = [] const projectIndex = buildProjectGroupingIndex(projectGrouping) @@ -1050,14 +1043,6 @@ export function buildRows( settings, projectGrouping }) - const getSectionActivity = ( - key: string - ): Pick => { - const summary = sectionActivitySummaries?.get(key) - return summary && summary.runningCount > 0 - ? { runningCount: summary.runningCount, hostRunningCounts: summary.hostRunningCounts } - : {} - } // Repos whose only visible worktrees are pinned are surfaced via the pinned // section, so the repo-grouped inbox loop below must not re-create a group for them. const visiblePinnedRepoIds = new Set( @@ -1071,8 +1056,7 @@ export function buildRows( renderedNaturalAnchorRepoIds, importedWorktreesByRepo, groupBy !== 'repo', - result, - sectionActivitySummaries?.get(PINNED_GROUP_KEY) + result ) if (groupBy === 'none') { if (naturalWorktrees.length > 0) { @@ -1085,8 +1069,7 @@ export function buildRows( icon: ALL_GROUP_META.icon, hostWorktreeCounts: getHostWorktreeCounts(naturalWorktrees, repoMap, defaultHostId), hostWorktreeIds: getHostWorktreeIds(naturalWorktrees, repoMap, defaultHostId), - worktreeIds: naturalWorktrees.map((worktree) => worktree.id), - ...getSectionActivity(ALL_GROUP_KEY) + worktreeIds: naturalWorktrees.map((worktree) => worktree.id) }) if (!collapsedGroups.has(ALL_GROUP_KEY)) { appendWorktreeRows(result, naturalWorktrees, repoMap, lineageById, worktreeMap, { @@ -1273,8 +1256,7 @@ export function buildRows( icon: meta.icon, hostWorktreeCounts: getHostWorktreeCounts(group.items, repoMap, defaultHostId), hostWorktreeIds: getHostWorktreeIds(group.items, repoMap, defaultHostId), - worktreeIds: group.items.map((worktree) => worktree.id), - ...getSectionActivity(key) + worktreeIds: group.items.map((worktree) => worktree.id) } })() : (() => { @@ -1289,8 +1271,7 @@ export function buildRows( icon: meta.icon, hostWorktreeCounts: getHostWorktreeCounts(group.items, repoMap, defaultHostId), hostWorktreeIds: getHostWorktreeIds(group.items, repoMap, defaultHostId), - worktreeIds: group.items.map((worktree) => worktree.id), - ...getSectionActivity(key) + worktreeIds: group.items.map((worktree) => worktree.id) } })() @@ -1438,8 +1419,7 @@ export function buildRows( tone: PROJECT_GROUP_META.tone, icon: PROJECT_GROUP_META.icon, projectGroup, - projectGroupDepth: depth, - ...getSectionActivity(key) + projectGroupDepth: depth }) if (!collapsedGroups.has(key)) { for (const folderWorkspace of folderWorkspacesByProjectGroupId.get(projectGroup.id) ?? []) { diff --git a/src/renderer/src/components/sidebar/worktree-section-activity.test.ts b/src/renderer/src/components/sidebar/worktree-section-activity.test.ts index b503ecf25d2..5de876fedb3 100644 --- a/src/renderer/src/components/sidebar/worktree-section-activity.test.ts +++ b/src/renderer/src/components/sidebar/worktree-section-activity.test.ts @@ -1,19 +1,10 @@ import { describe, expect, it } from 'vitest' import type { AgentStatusEntry } from '../../../../shared/agent-status-types' import { makePaneKey } from '../../../../shared/stable-pane-id' -import type { - Project, - ProjectGroup, - ProjectHostSetup, - Repo, - TerminalTab, - Worktree -} from '../../../../shared/types' +import type { ProjectGroup, Repo, TerminalTab, Worktree } from '../../../../shared/types' import { getProjectGroupHeaderKey, PINNED_GROUP_KEY, - type PinnedWorktreeDisplayPolicy, - type ProjectGroupingModel, type WorktreeGroupBy } from './worktree-list-groups' import { @@ -100,7 +91,6 @@ function makeState( agentStatusByPaneKey: {}, migrationUnsupportedByPtyId: {}, retainedAgentsByPaneKey: {}, - runtimeAgentOrchestrationByPaneKey: {}, ...overrides } } @@ -110,18 +100,12 @@ function buildSummaries({ worktrees, repos, projectGroups = [], - projectGrouping, - pinnedDisplayPolicy, - defaultHostId, state }: { groupBy?: WorktreeGroupBy worktrees: readonly Worktree[] repos: readonly Repo[] projectGroups?: readonly ProjectGroup[] - projectGrouping?: ProjectGroupingModel - pinnedDisplayPolicy?: PinnedWorktreeDisplayPolicy - defaultHostId?: Parameters[0]['defaultHostId'] state: WorktreeSectionActivityState }) { return buildWorktreeSectionActivitySummaries({ @@ -131,9 +115,6 @@ function buildSummaries({ prCache: null, workspaceStatuses: [], projectGroups, - projectGrouping, - pinnedDisplayPolicy, - defaultHostId, state }) } @@ -167,20 +148,17 @@ describe('buildWorktreeSectionActivitySummaries', () => { }) expect(summaries.get(getProjectGroupHeaderKey(parent.id))).toEqual({ - runningCount: 1, - hostRunningCounts: new Map([['local', 1]]) + runningCount: 1 }) expect(summaries.get(getProjectGroupHeaderKey(child.id))).toEqual({ - runningCount: 1, - hostRunningCounts: new Map([['local', 1]]) + runningCount: 1 }) expect(summaries.get(`repo:${repo.id}`)).toEqual({ - runningCount: 1, - hostRunningCounts: new Map([['local', 1]]) + runningCount: 1 }) }) - it('counts pinned workspace activity only on the pinned header', () => { + it('counts pinned workspace activity on pinned and natural headers', () => { const repo = makeRepo({ projectGroupId: 'group-1' }) const worktree = makeWorktree({ repoId: repo.id, isPinned: true }) const now = Date.now() @@ -209,170 +187,14 @@ describe('buildWorktreeSectionActivitySummaries', () => { state }) - const pinnedSummary = summaries.get(PINNED_GROUP_KEY) - expect(pinnedSummary?.runningCount).toBe(1) - expect(pinnedSummary?.hostRunningCounts).toEqual(new Map([['local', 1]])) - expect(summaries.get(`repo:${repo.id}`)).toBeUndefined() - expect(summaries.get(getProjectGroupHeaderKey('group-1'))).toBeUndefined() - }) - - it('counts pinned activity in natural groups when duplicate display is enabled', () => { - const repo = makeRepo({ projectGroupId: 'group-1' }) - const worktree = makeWorktree({ repoId: repo.id, isPinned: true }) - const now = Date.now() - const entry: AgentStatusEntry = { - state: 'working', - prompt: '', - updatedAt: now, - stateStartedAt: now, - paneKey: makePaneKey('tab-1', LEAF_ID), - stateHistory: [] - } - const state = makeState({ - tabsByWorktree: { - [worktree.id]: [makeTerminalTab({ id: 'tab-1', worktreeId: worktree.id })] - }, - agentStatusEpoch: 1, - agentStatusByPaneKey: { - [entry.paneKey]: entry - } + expect(summaries.get(PINNED_GROUP_KEY)).toEqual({ + runningCount: 1 }) - - const summaries = buildSummaries({ - worktrees: [worktree], - repos: [repo], - projectGroups: [makeProjectGroup({ id: 'group-1' })], - pinnedDisplayPolicy: 'duplicate-in-groups', - state + expect(summaries.get(`repo:${repo.id}`)).toEqual({ + runningCount: 1 }) - - expect(summaries.get(PINNED_GROUP_KEY)?.runningCount).toBe(1) - expect(summaries.get(`repo:${repo.id}`)?.runningCount).toBe(1) - expect(summaries.get(`repo:${repo.id}`)?.hostRunningCounts).toEqual(new Map([['local', 1]])) - expect(summaries.get(getProjectGroupHeaderKey('group-1'))?.runningCount).toBe(1) - expect(summaries.get(getProjectGroupHeaderKey('group-1'))?.hostRunningCounts).toEqual( - new Map([['local', 1]]) - ) - }) - - it('counts duplicate-mode pinned activity on project setup headers', () => { - const repo = makeRepo() - const project: Project = { - id: 'github:stablyai/orca', - displayName: 'Orca', - badgeColor: '#737373', - sourceRepoIds: [repo.id], - createdAt: 1, - updatedAt: 1 - } - const setup: ProjectHostSetup = { - id: repo.id, - projectId: project.id, - hostId: 'local', - repoId: repo.id, - path: repo.path, - displayName: repo.displayName, - setupState: 'ready', - setupMethod: 'legacy-repo', - createdAt: 1, - updatedAt: 1 - } - const worktree = makeWorktree({ repoId: repo.id, isPinned: true }) - const state = makeState({ - tabsByWorktree: { - [worktree.id]: [ - makeTerminalTab({ id: 'tab-1', worktreeId: worktree.id, title: 'Codex working' }) - ] - }, - ptyIdsByTabId: { - 'tab-1': ['pty-1'] - } + expect(summaries.get(getProjectGroupHeaderKey('group-1'))).toEqual({ + runningCount: 1 }) - - const summaries = buildSummaries({ - worktrees: [worktree], - repos: [repo], - projectGrouping: { projects: [project], projectHostSetups: [setup] }, - pinnedDisplayPolicy: 'duplicate-in-groups', - state - }) - - expect(summaries.get(PINNED_GROUP_KEY)?.runningCount).toBe(1) - expect(summaries.get(`project:${project.id}`)).toEqual({ - runningCount: 1, - hostRunningCounts: new Map([['local', 1]]) - }) - expect(summaries.get(`repo:${repo.id}`)).toBeUndefined() - }) - - it('keeps running activity attributable to owning hosts per section', () => { - const localRepo = makeRepo({ id: 'local-repo' }) - const sshRepo = makeRepo({ - id: 'ssh-repo', - connectionId: 'ssh-1' - }) - const localWorktree = makeWorktree({ - id: 'local-wt', - repoId: localRepo.id, - isPinned: true - }) - const sshWorktree = makeWorktree({ - id: 'ssh-wt', - repoId: sshRepo.id, - isPinned: true - }) - const state = makeState({ - tabsByWorktree: { - [localWorktree.id]: [ - makeTerminalTab({ - id: 'local-tab', - worktreeId: localWorktree.id, - title: 'Codex working' - }) - ], - [sshWorktree.id]: [ - makeTerminalTab({ - id: 'ssh-tab', - worktreeId: sshWorktree.id, - title: 'Codex working' - }) - ] - }, - ptyIdsByTabId: { - 'local-tab': ['local-pty'], - 'ssh-tab': ['ssh-pty'] - } - }) - - const summaries = buildSummaries({ - worktrees: [localWorktree, sshWorktree], - repos: [localRepo, sshRepo], - defaultHostId: 'local', - state - }) - - const pinnedSummary = summaries.get(PINNED_GROUP_KEY) - expect(pinnedSummary?.runningCount).toBe(2) - expect(pinnedSummary?.hostRunningCounts).toEqual( - new Map([ - ['local', 1], - ['ssh:ssh-1', 1] - ]) - ) - const allSummary = buildSummaries({ - groupBy: 'none', - worktrees: [localWorktree, sshWorktree], - repos: [localRepo, sshRepo], - pinnedDisplayPolicy: 'duplicate-in-groups', - defaultHostId: 'local', - state - }).get('all') - expect(allSummary?.runningCount).toBe(2) - expect(allSummary?.hostRunningCounts).toEqual( - new Map([ - ['local', 1], - ['ssh:ssh-1', 1] - ]) - ) }) }) diff --git a/src/renderer/src/components/sidebar/worktree-section-activity.ts b/src/renderer/src/components/sidebar/worktree-section-activity.ts index e36ebc984fa..fb81da4f0cf 100644 --- a/src/renderer/src/components/sidebar/worktree-section-activity.ts +++ b/src/renderer/src/components/sidebar/worktree-section-activity.ts @@ -8,17 +8,9 @@ import type { Worktree, WorkspaceStatusDefinition } from '../../../../shared/types' -import { - getRepoExecutionHostId, - LOCAL_EXECUTION_HOST_ID, - type ExecutionHostId -} from '../../../../shared/execution-host' import { getGroupKeysForWorktree, - getPinnedWorktreeDisplayPolicy, - type ProjectGroupingModel, type WorktreeGroupBy, - type PinnedWorktreeDisplayPolicy, PINNED_GROUP_KEY } from './worktree-list-groups' import { @@ -36,7 +28,6 @@ export type WorktreeSectionActivityState = Pick< | 'agentStatusByPaneKey' | 'migrationUnsupportedByPtyId' | 'retainedAgentsByPaneKey' - | 'runtimeAgentOrchestrationByPaneKey' > & { tabsByWorktree: Record[]> browserTabsByWorktree: Record @@ -45,7 +36,6 @@ export type WorktreeSectionActivityState = Pick< export type WorktreeSectionActivitySummary = { runningCount: number - hostRunningCounts?: ReadonlyMap } export const EMPTY_WORKTREE_SECTION_ACTIVITY: WorktreeSectionActivitySummary = { @@ -60,9 +50,6 @@ export function buildWorktreeSectionActivitySummaries({ workspaceStatuses, settings, projectGroups, - projectGrouping, - pinnedDisplayPolicy = getPinnedWorktreeDisplayPolicy(settings), - defaultHostId = LOCAL_EXECUTION_HOST_ID, state }: { groupBy: WorktreeGroupBy @@ -72,30 +59,23 @@ export function buildWorktreeSectionActivitySummaries({ workspaceStatuses: readonly WorkspaceStatusDefinition[] settings?: AppState['settings'] projectGroups: readonly ProjectGroup[] - projectGrouping?: ProjectGroupingModel - pinnedDisplayPolicy?: PinnedWorktreeDisplayPolicy - defaultHostId?: ExecutionHostId state: WorktreeSectionActivityState }): Map { const summaries = new Map() for (const worktree of worktrees) { - const naturalGroupKeys = getGroupKeysForWorktree( - groupBy, - worktree, - repoMap, - prCache, - workspaceStatuses, - settings, - projectGroups, - projectGrouping - ) - const groupKeys = - worktree.isPinned && pinnedDisplayPolicy === 'single-location' - ? [PINNED_GROUP_KEY] - : worktree.isPinned - ? [PINNED_GROUP_KEY, ...naturalGroupKeys] - : naturalGroupKeys + const groupKeys = [ + ...(worktree.isPinned ? [PINNED_GROUP_KEY] : []), + ...getGroupKeysForWorktree( + groupBy, + worktree, + repoMap, + prCache, + workspaceStatuses, + settings, + projectGroups + ) + ] if (groupKeys.length === 0) { continue } @@ -105,10 +85,6 @@ export function buildWorktreeSectionActivitySummaries({ const summary = summaries.get(groupKey) ?? { ...EMPTY_WORKTREE_SECTION_ACTIVITY } if (status === 'working') { summary.runningCount++ - const hostRunningCounts = new Map(summary.hostRunningCounts ?? []) - const hostId = getWorktreeHostId(worktree, repoMap, defaultHostId) - hostRunningCounts.set(hostId, (hostRunningCounts.get(hostId) ?? 0) + 1) - summary.hostRunningCounts = hostRunningCounts } summaries.set(groupKey, summary) } @@ -117,20 +93,6 @@ export function buildWorktreeSectionActivitySummaries({ return summaries } -function getWorktreeHostId( - worktree: Worktree, - repoMap: ReadonlyMap, - defaultHostId: ExecutionHostId -): ExecutionHostId { - const repo = repoMap.get(worktree.repoId) - // Why: pinned headers can be cloned inside host sections, so hidden pinned - // activity must stay attributed to the owning host instead of the global key. - if (repo?.connectionId || repo?.executionHostId) { - return getRepoExecutionHostId(repo) - } - return defaultHostId -} - function getSectionWorktreeStatus( state: WorktreeSectionActivityState, worktreeId: string diff --git a/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts new file mode 100644 index 00000000000..cd4672dc9a7 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest' +import type { FolderWorkspace, ProjectGroup, Repo, Worktree } from '../../../../shared/types' +import type { HostSectionRow } from './host-section-rows' +import { getRenderedWorktreesInSidebarOrder } from './worktree-sidebar-row-preference' + +const repo: Repo = { + id: 'repo-1', + path: '/repo-1', + displayName: 'Repo 1', + badgeColor: '#737373', + addedAt: 1 +} + +function worktree(id: string, isPinned = false): Worktree { + return { + id, + repoId: repo.id, + path: `/repo-1/${id}`, + displayName: id, + branch: id, + head: 'abc123', + isBare: false, + isMainWorktree: false, + comment: '', + linkedIssue: null, + linkedPR: null, + linkedLinearIssue: null, + isArchived: false, + isUnread: false, + isPinned, + sortOrder: 1, + lastActivityAt: 1 + } +} + +function item(workspace: Worktree, sectionKey: string): HostSectionRow { + return { + type: 'item', + rowKey: `${sectionKey}:${workspace.id}`, + sectionKey, + worktree: workspace, + repo, + depth: 0, + groupDepth: 0, + lineageTrail: [], + isLastLineageChild: false, + lineageChildCount: 0 + } +} + +const projectGroup: ProjectGroup = { + id: 'group-1', + name: 'Group 1', + parentPath: '/group-1', + parentGroupId: null, + createdFrom: 'folder-scan', + tabOrder: 0, + isCollapsed: false, + color: null, + createdAt: 1, + updatedAt: 1 +} + +const folderWorkspace: FolderWorkspace = { + id: 'folder-1', + projectGroupId: projectGroup.id, + name: 'Folder 1', + folderPath: '/group-1/folder-1', + linkedTask: null, + comment: '', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 1, + lastActivityAt: 1, + createdAt: 1, + updatedAt: 1 +} + +describe('getRenderedWorktreesInSidebarOrder', () => { + it('keeps folder workspaces in visual order while preferring natural pinned rows', () => { + const pinned = worktree('pinned', true) + const afterFolder = worktree('after-folder') + const rows: HostSectionRow[] = [ + item(pinned, 'pinned'), + { + type: 'folder-workspace', + key: 'folder-workspace:folder-1', + folderWorkspace, + projectGroup, + depth: 0, + groupDepth: 0 + }, + item(pinned, 'repo:repo-1'), + item(afterFolder, 'repo:repo-1') + ] + + expect( + getRenderedWorktreesInSidebarOrder(rows, 'duplicate-in-groups').map(({ id }) => id) + ).toEqual(['folder:folder-1', 'pinned', 'after-folder']) + }) +}) diff --git a/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts new file mode 100644 index 00000000000..c73107cbed8 --- /dev/null +++ b/src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts @@ -0,0 +1,64 @@ +import { folderWorkspaceToWorktree } from '../../../../shared/folder-workspace-worktree' +import type { Worktree } from '../../../../shared/types' +import type { HostSectionRow } from './host-section-rows' +import { + PINNED_GROUP_KEY, + type PinnedWorktreeDisplayPolicy, + type WorktreeRow +} from './worktree-list-groups' + +export function getPreferredWorktreeRows( + rows: readonly WorktreeRow[], + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy +): WorktreeRow[] { + // Why: duplicated pinned rows represent one workspace to navigation and selection. + // Prefer its natural-group copy, falling back to Pinned when that copy is hidden. + if (pinnedDisplayPolicy === 'single-location') { + const seen = new Set() + return rows.filter((row) => { + if (seen.has(row.worktree.id)) { + return false + } + seen.add(row.worktree.id) + return true + }) + } + + const preferredRows: WorktreeRow[] = [] + const seen = new Set() + for (const row of rows) { + if (row.sectionKey === PINNED_GROUP_KEY || seen.has(row.worktree.id)) { + continue + } + preferredRows.push(row) + seen.add(row.worktree.id) + } + for (const row of rows) { + if (seen.has(row.worktree.id)) { + continue + } + preferredRows.push(row) + seen.add(row.worktree.id) + } + return preferredRows +} + +export function getRenderedWorktreesInSidebarOrder( + rows: readonly HostSectionRow[], + pinnedDisplayPolicy: PinnedWorktreeDisplayPolicy +): Worktree[] { + const itemRows = rows.filter((row): row is WorktreeRow => row.type === 'item') + const preferredRowKeys = new Set( + getPreferredWorktreeRows(itemRows, pinnedDisplayPolicy).map((row) => row.rowKey) + ) + const renderedWorktrees: Worktree[] = [] + + for (const row of rows) { + if (row.type === 'item' && preferredRowKeys.has(row.rowKey)) { + renderedWorktrees.push(row.worktree) + } else if (row.type === 'folder-workspace') { + renderedWorktrees.push(folderWorkspaceToWorktree(row.folderWorkspace)) + } + } + return renderedWorktrees +}