From 67fe4bd78092cc3ec8aa01b9d6c7f89ded2de6d8 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:23:21 -0700 Subject: [PATCH] Fix manual project ordering for identity headers (#5933) Co-authored-by: Orca --- .../sidebar/worktree-list-groups.test.ts | 76 +++++++++++++++++++ .../sidebar/worktree-list-groups.ts | 42 +++++++++- 2 files changed, 115 insertions(+), 3 deletions(-) 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 d3e518b5fbc..cb667f6f1d5 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts @@ -361,6 +361,82 @@ describe('buildRows with pinned worktrees', () => { ]) }) + it('orders project identity headers by the manual repo order anchor', () => { + const analyticsProject: Project = { + ...project, + id: 'github:stablyai/analytics', + displayName: 'Analytics', + sourceRepoIds: ['repo-analytics'] + } + const analyticsRepo: Repo = { + ...repo, + id: 'repo-analytics', + path: '/tmp/analytics', + displayName: 'analytics', + upstream: { owner: 'stablyai', repo: 'analytics' } + } + const analyticsWorktree: Worktree = { + ...worktree, + id: 'wt-analytics', + repoId: analyticsRepo.id, + displayName: 'analytics' + } + const analyticsSetup: ProjectHostSetup = { + ...projectHostSetups[0]!, + id: analyticsRepo.id, + projectId: analyticsProject.id, + repoId: analyticsRepo.id, + path: analyticsRepo.path, + displayName: analyticsRepo.displayName + } + const repoOrder = new Map([ + [repo.id, 0], + [remoteRepo.id, 1], + [analyticsRepo.id, 2] + ]) + + const rows = buildRows( + 'repo', + [worktree, analyticsWorktree, remoteWorktree], + new Map([ + [repo.id, repo], + [remoteRepo.id, remoteRepo], + [analyticsRepo.id, analyticsRepo] + ]), + null, + new Set(), + repoOrder, + undefined, + 'manual', + {}, + new Map([ + [worktree.id, worktree], + [remoteWorktree.id, remoteWorktree], + [analyticsWorktree.id, analyticsWorktree] + ]), + false, + undefined, + [], + new Set(), + new Map(), + [], + { + projects: [project, analyticsProject], + projectHostSetups: [...projectHostSetups, analyticsSetup] + } + ) + + const headers = rows.filter((row) => row.type === 'header') + expect(headers.map((row) => row.key)).toEqual([ + 'project:github:stablyai/orca', + 'project:github:stablyai/analytics' + ]) + expect(headers[0]).toMatchObject({ + key: 'project:github:stablyai/orca', + repo: { id: repo.id, badgeColor: repo.badgeColor } + }) + }) + it('splits same-host checkouts of one project into separate per-setup groups', () => { // Why: multiple local clones/worktrees of one repo share the GitHub slug, so // collapsing to the project would merge them into one arbitrarily-named group. diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.ts b/src/renderer/src/components/sidebar/worktree-list-groups.ts index 25cf6468c12..190ef5bcebc 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.ts @@ -673,9 +673,39 @@ function manualRankForEntry( repoOrder: Map | undefined ): number { const key = entry[0] - const repoId = key.startsWith('repo:') ? key.slice('repo:'.length) : key - const rank = repoOrder?.get(repoId) - return rank === undefined ? Number.POSITIVE_INFINITY : rank + const repoIds = + entry[1].repoIds.size > 0 + ? [...entry[1].repoIds] + : [key.startsWith('repo:') ? key.slice('repo:'.length) : key] + let rank = Number.POSITIVE_INFINITY + for (const repoId of repoIds) { + const repoRank = repoOrder?.get(repoId) + if (repoRank !== undefined && repoRank < rank) { + rank = repoRank + } + } + return rank +} + +function getManualOrderAnchorRepo( + group: WorktreeGroupEntry, + repoMap: Map, + repoOrder: Map | undefined +): Repo | undefined { + let anchor = group.repo + let anchorRank = anchor ? (repoOrder?.get(anchor.id) ?? Number.POSITIVE_INFINITY) : undefined + for (const repoId of group.repoIds) { + const repo = repoMap.get(repoId) + if (!repo) { + continue + } + const rank = repoOrder?.get(repoId) ?? Number.POSITIVE_INFINITY + if (!anchor || rank < (anchorRank ?? Number.POSITIVE_INFINITY)) { + anchor = repo + anchorRank = rank + } + } + return anchor } /** @@ -904,6 +934,12 @@ export function buildRows( } } } else { + for (const group of grouped.values()) { + // Why: logical project headers can contain multiple host setup repos. + // Use the repo that anchors manual order so drag and actions target the + // same persisted order source the row sorter reads. + group.repo = getManualOrderAnchorRepo(group, repoMap, repoOrder) + } // Why: project header order is its own user choice (projectOrderBy), // decoupled from workspace sortBy. Manual uses the canonical repoOrder so // header drag has a stable source of truth; Recent follows activity.