diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index 74141e5ef..77b5a2bac 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -190,6 +190,7 @@ class SessionSelectorHeader implements Component { interface SessionTreeNode { session: SessionInfo; children: SessionTreeNode[]; + latestActivity: number; } /** Flattened node for display with tree structure info */ @@ -210,7 +211,7 @@ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { for (const session of sessions) { const sessionPath = canonicalizePath(session.path) ?? session.path; - byPath.set(sessionPath, { session, children: [] }); + byPath.set(sessionPath, { session, children: [], latestActivity: session.modified.getTime() }); } const roots: SessionTreeNode[] = []; @@ -227,9 +228,22 @@ function buildSessionTree(sessions: SessionInfo[]): SessionTreeNode[] { } } - // Sort children and roots by modified date (descending) + const updateLatestActivity = (node: SessionTreeNode): number => { + let latestActivity = node.session.modified.getTime(); + for (const child of node.children) { + latestActivity = Math.max(latestActivity, updateLatestActivity(child)); + } + node.latestActivity = latestActivity; + return latestActivity; + }; + + for (const root of roots) { + updateLatestActivity(root); + } + + // Sort children and roots by latest activity in each subtree (descending) const sortNodes = (nodes: SessionTreeNode[]): void => { - nodes.sort((a, b) => b.session.modified.getTime() - a.session.modified.getTime()); + nodes.sort((a, b) => b.latestActivity - a.latestActivity); for (const node of nodes) { sortNodes(node.children); } diff --git a/packages/coding-agent/test/session-selector-path-delete.test.ts b/packages/coding-agent/test/session-selector-path-delete.test.ts index 52cae4860..cb702d5e2 100644 --- a/packages/coding-agent/test/session-selector-path-delete.test.ts +++ b/packages/coding-agent/test/session-selector-path-delete.test.ts @@ -282,6 +282,45 @@ describe("session selector path/delete interactions", () => { expect(output).toContain("└─ Child"); }); + it("sorts threaded sessions by latest activity in their subtree", async () => { + const parentOne = makeSession({ + id: "parent-one", + name: "Parent one", + modified: new Date("2026-01-02T00:00:00.000Z"), + }); + const parentTwo = makeSession({ + id: "parent-two", + name: "Parent two", + modified: new Date("2026-01-01T00:00:00.000Z"), + }); + const childTwo = makeSession({ + id: "child-two", + name: "Child two", + parentSessionPath: parentTwo.path, + modified: new Date("2026-01-03T00:00:00.000Z"), + }); + + const selector = new SessionSelectorComponent( + async () => [parentOne, parentTwo, childTwo], + async () => [], + () => {}, + () => {}, + () => {}, + () => {}, + { keybindings }, + ); + await flushPromises(); + + const output = stripAnsi(selector.render(120).join("\n")); + const parentTwoIndex = output.indexOf("Parent two"); + const childTwoIndex = output.indexOf("└─ Child two"); + const parentOneIndex = output.indexOf("Parent one"); + + expect(parentTwoIndex).toBeGreaterThanOrEqual(0); + expect(childTwoIndex).toBeGreaterThan(parentTwoIndex); + expect(parentOneIndex).toBeGreaterThan(childTwoIndex); + }); + it("treats the current session as active across symlink aliases", async () => { const paths = createSymlinkedSessionPaths(); tempDirs.push(paths.baseDir);