From 8557cb1d8bd3fb34feea3e931ea6ad9acb2a4f39 Mon Sep 17 00:00:00 2001 From: Michael Yong Date: Fri, 18 Sep 2026 22:28:09 -0700 Subject: [PATCH] Fix environment grouping across custom sidebar sections --- .../src/sidebar/projectThreadGroups.ts | 24 ++++- .../test/projectThreadGroups.test.ts | 97 +++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/packages/client-core/src/sidebar/projectThreadGroups.ts b/packages/client-core/src/sidebar/projectThreadGroups.ts index a6916d6c098..f13a882634b 100644 --- a/packages/client-core/src/sidebar/projectThreadGroups.ts +++ b/packages/client-core/src/sidebar/projectThreadGroups.ts @@ -211,7 +211,24 @@ function buildSortedItems( compareThreads: ThreadComparator, groupEnvironmentThreads: boolean, draftThreadIds: ReadonlySet, + respectSections = false, ): ProjectThreadItem[] { + if (groupEnvironmentThreads && respectSections) { + const nodesBySectionId = new Map(); + for (const node of nodes) { + const sectionId = node.thread.sectionId; + const bucket = nodesBySectionId.get(sectionId); + if (bucket) { + bucket.push(node); + } else { + nodesBySectionId.set(sectionId, [node]); + } + } + return [...nodesBySectionId.values()].flatMap((sectionNodes) => + buildSortedItems(sectionNodes, compareThreads, true, draftThreadIds), + ); + } + if (!groupEnvironmentThreads) { nodes.sort((left, right) => compareThreads(left.thread, right.thread)); return nodes.map(buildThreadItem); @@ -353,6 +370,7 @@ function buildThreadTreeItems( compareThreads: ThreadComparator, groupEnvironmentThreads: boolean, draftThreadIds: ReadonlySet, + respectSections = false, ): ProjectThreadItem[] { const projectThreads = allThreads.filter(isSidebarProjectThread); const projectThreadIds = new Set(projectThreads.map((thread) => thread.id)); @@ -413,6 +431,7 @@ function buildThreadTreeItems( compareThreads, groupEnvironmentThreads, draftThreadIds, + respectSections, ); } @@ -438,11 +457,12 @@ export function buildSectionThreadList( groupEnvironmentThreads = false, ): ProjectThreadItem[] { return bucketIntoSections( - buildChronologicalThreadList( + buildThreadTreeItems( allThreads, compareThreads, - draftThreadIds, groupEnvironmentThreads, + draftThreadIds, + true, ), CHRONOLOGICAL_CONTAINER_ID, compareThreads, diff --git a/packages/client-core/test/projectThreadGroups.test.ts b/packages/client-core/test/projectThreadGroups.test.ts index 6922cfce00e..ff39a26160a 100644 --- a/packages/client-core/test/projectThreadGroups.test.ts +++ b/packages/client-core/test/projectThreadGroups.test.ts @@ -669,6 +669,103 @@ describe("worktree grouping preference", () => { ]); }); + it.each([false, true])( + "respects sections with environment grouping %s", + (groupEnvironmentThreads) => { + const threads = [ + ...worktreeSiblings, + createThread({ + id: "later", + environmentId: "env_wt", + environmentIsWorktree: true, + sectionId: "sec_later", + createdAt: 30, + }), + createThread({ + id: "loose", + environmentId: "env_wt", + environmentIsWorktree: true, + createdAt: 40, + }), + ]; + const items = buildSectionThreadList( + threads, + compareStandardThreads, + [...sections, { id: "sec_later", name: "Later" }], + new Set(), + groupEnvironmentThreads, + ); + expect(summarizeItems(items)).toEqual([ + { + section: "chronological::sec_work", + name: "Work", + items: groupEnvironmentThreads + ? [{ env: "env_wt", threads: ["wt-b", "wt-a"] }] + : ["wt-b", "wt-a"], + }, + { + section: "chronological::sec_later", + name: "Later", + items: ["later"], + }, + "loose", + ]); + expect(summarizeItems(buildProjectThreadGroups(threads))).toEqual([ + { env: "env_wt", threads: ["loose", "later", "wt-b", "wt-a"] }, + ]); + }, + ); + + it("keeps separate environment groups and their descendants in each section", () => { + const threads = [ + ...worktreeSiblings, + createThread({ + id: "later-a", + environmentId: "env_wt", + environmentIsWorktree: true, + sectionId: "sec_later", + createdAt: 30, + }), + createThread({ + id: "later-b", + environmentId: "env_wt", + environmentIsWorktree: true, + sectionId: "sec_later", + createdAt: 40, + }), + createThread({ id: "child", parentThreadId: "wt-a", createdAt: 50 }), + ]; + const items = buildSectionThreadList( + threads, + compareStandardThreads, + sections, + new Set(), + true, + ); + expect(summarizeItems(items)).toEqual([ + { + section: "chronological::sec_work", + name: "Work", + items: [ + { + env: "env_wt", + threads: ["wt-b", { id: "wt-a", children: ["child"] }], + }, + ], + }, + { + section: "chronological::sec_later", + name: "Section", + items: [{ env: "env_wt", threads: ["later-b", "later-a"] }], + }, + ]); + expect( + items.map((item) => + item.kind === "section" ? item.group.threadCount : 0, + ), + ).toEqual([3, 2]); + }); + it("keeps worktree siblings flat inside a section when disabled", () => { const items = buildSectionThreadList( worktreeSiblings,