From c43c233a07b423d9ffdb1a4892d542bbd6a77ad2 Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Thu, 16 Jul 2026 19:38:13 -0700
Subject: [PATCH] Fix pinned sidebar order and render churn
---
.../src/components/sidebar/WorktreeList.tsx | 153 +-------------
.../sidebar/host-section-rows.test.ts | 6 +-
.../components/sidebar/host-section-rows.ts | 8 +-
.../sidebar/worktree-list-groups.test.ts | 58 -----
.../sidebar/worktree-list-groups.ts | 36 +---
.../sidebar/worktree-section-activity.test.ts | 200 +-----------------
.../sidebar/worktree-section-activity.ts | 62 ++----
.../worktree-sidebar-row-preference.test.ts | 102 +++++++++
.../worktree-sidebar-row-preference.ts | 64 ++++++
9 files changed, 212 insertions(+), 477 deletions(-)
create mode 100644 src/renderer/src/components/sidebar/worktree-sidebar-row-preference.test.ts
create mode 100644 src/renderer/src/components/sidebar/worktree-sidebar-row-preference.ts
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 (
-
-
-
-
- {count}
-
-
-
- {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
+}