From bc3e9b7b0abba2aa029ae05934c5f9e42c103a78 Mon Sep 17 00:00:00 2001 From: Ido Frizler Date: Thu, 19 Feb 2026 15:01:08 +0200 Subject: [PATCH 1/2] fix: deduplicate worktree sessions in session history Previous sessions from the SDK were not enriched with worktree data (matched by cwd), so they weren't added to coveredWorktreePaths. This caused standalone worktree entries to be added on top of existing SDK sessions for the same branch. Additionally, multiple SDK sessions pointing to the same worktree were all shown as separate entries. Now only the most recent session per worktree path is kept. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SessionHistory/SessionHistory.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/SessionHistory/SessionHistory.tsx b/src/renderer/components/SessionHistory/SessionHistory.tsx index a8dcacb..337b3c9 100644 --- a/src/renderer/components/SessionHistory/SessionHistory.tsx +++ b/src/renderer/components/SessionHistory/SessionHistory.tsx @@ -303,10 +303,40 @@ export const SessionHistory: React.FC = ({ }); // Filter out any previous sessions that are now active (shouldn't happen but just in case) + // Enrich previous sessions with worktree data from worktreeMap (matched by cwd) const activeIds = new Set(activeSessions.map((t) => t.id)); - const filteredPrevious: DisplaySession[] = sessions + const enrichedPrevious: DisplaySession[] = sessions .filter((s) => !activeIds.has(s.sessionId)) - .map((s) => ({ ...s, isActive: false })); + .map((s) => { + const worktreeData = s.cwd ? worktreeMap.get(s.cwd) : undefined; + const worktree = worktreeData + ? { + id: worktreeData.id, + repoPath: worktreeData.repoPath, + branch: worktreeData.branch, + worktreePath: worktreeData.worktreePath, + status: worktreeData.status, + diskUsage: worktreeData.diskUsage, + } + : s.worktree; + return { ...s, isActive: false, worktree }; + }); + + // Deduplicate: for sessions sharing the same worktree, keep only the most recent + const bestWorktreeSession = new Map(); + for (const session of enrichedPrevious) { + const wtPath = session.worktree?.worktreePath; + if (!wtPath) continue; + const existing = bestWorktreeSession.get(wtPath); + if (!existing || new Date(session.modifiedTime) > new Date(existing.modifiedTime)) { + bestWorktreeSession.set(wtPath, session); + } + } + const filteredPrevious = enrichedPrevious.filter((session) => { + const wtPath = session.worktree?.worktreePath; + if (!wtPath) return true; + return bestWorktreeSession.get(wtPath)?.sessionId === session.sessionId; + }); // Collect worktree paths that are already represented const coveredWorktreePaths = new Set(); From 4ce6f620a97247399103e3cda8579afce90fa9eb Mon Sep 17 00:00:00 2001 From: Ido Frizler Date: Thu, 19 Feb 2026 15:04:33 +0200 Subject: [PATCH 2/2] test: add worktree session deduplication tests Tests cover: - Enriching previous sessions with worktree data prevents standalone duplicates - Multiple SDK sessions for the same worktree are deduplicated (keep most recent) - Sessions with different worktree paths are not deduplicated Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/components/SessionHistory.test.tsx | 147 +++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/tests/components/SessionHistory.test.tsx b/tests/components/SessionHistory.test.tsx index 0f29d15..e68fd19 100644 --- a/tests/components/SessionHistory.test.tsx +++ b/tests/components/SessionHistory.test.tsx @@ -851,4 +851,151 @@ describe('SessionHistory Component', () => { expect(screen.getByText('2')).toBeInTheDocument(); }); }); + + describe('Worktree Session Deduplication', () => { + const worktreePath = '/Users/dev/.copilot-sessions/repo--feature-branch'; + const worktreeListResult = { + sessions: [ + { + id: 'repo--feature-branch', + repoPath: '/Users/dev/repo', + branch: 'feature/branch', + worktreePath, + status: 'active' as const, + lastAccessedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + }, + ], + }; + + it('enriches previous sessions with worktree data from worktreeMap and prevents standalone duplicate', async () => { + // Mock listSessions to return a worktree matching the session's cwd + (window.electronAPI.worktree.listSessions as ReturnType).mockResolvedValue( + worktreeListResult + ); + + // Session has cwd matching worktree but no worktree property + const sessions: PreviousSession[] = [ + createMockSession('session-1', 'Work on feature', 0, worktreePath), + ]; + + await renderAndSettle( + + ); + + // Wait for worktreeMap to be populated via useEffect + await waitFor(() => { + // Should show the branch name (enriched from worktreeMap) + expect(screen.getByText('feature/branch')).toBeInTheDocument(); + }); + + // Should only appear once — no standalone worktree duplicate + const branchElements = screen.getAllByText('feature/branch'); + expect(branchElements).toHaveLength(1); + }); + + it('deduplicates previous sessions sharing the same worktree path', async () => { + (window.electronAPI.worktree.listSessions as ReturnType).mockResolvedValue( + worktreeListResult + ); + + const olderDate = new Date(); + olderDate.setHours(olderDate.getHours() - 2); + + // Two SDK sessions pointing to the same worktree + const sessions: PreviousSession[] = [ + { + sessionId: 'session-older', + name: 'Older session on branch', + modifiedTime: olderDate.toISOString(), + cwd: worktreePath, + }, + { + sessionId: 'session-newer', + name: 'Newer session on branch', + modifiedTime: new Date().toISOString(), + cwd: worktreePath, + }, + ]; + + await renderAndSettle( + + ); + + await waitFor(() => { + expect(screen.getByText('feature/branch')).toBeInTheDocument(); + }); + + // Only the most recent session should remain + expect(screen.getByText('Newer session on branch')).toBeInTheDocument(); + expect(screen.queryByText('Older session on branch')).not.toBeInTheDocument(); + + // Branch should appear exactly once (no standalone duplicate either) + expect(screen.getAllByText('feature/branch')).toHaveLength(1); + }); + + it('does not deduplicate sessions with different worktree paths', async () => { + const worktreePath2 = '/Users/dev/.copilot-sessions/repo--other-branch'; + (window.electronAPI.worktree.listSessions as ReturnType).mockResolvedValue({ + sessions: [ + ...worktreeListResult.sessions, + { + id: 'repo--other-branch', + repoPath: '/Users/dev/repo', + branch: 'other/branch', + worktreePath: worktreePath2, + status: 'active' as const, + lastAccessedAt: new Date().toISOString(), + createdAt: new Date().toISOString(), + }, + ], + }); + + const sessions: PreviousSession[] = [ + createMockSession('session-1', 'Work on feature', 0, worktreePath), + createMockSession('session-2', 'Work on other', 0, worktreePath2), + ]; + + await renderAndSettle( + + ); + + await waitFor(() => { + expect(screen.getByText('feature/branch')).toBeInTheDocument(); + }); + + // Both sessions should be visible — different worktrees + expect(screen.getByText('Work on feature')).toBeInTheDocument(); + expect(screen.getByText('Work on other')).toBeInTheDocument(); + expect(screen.getByText('feature/branch')).toBeInTheDocument(); + expect(screen.getByText('other/branch')).toBeInTheDocument(); + }); + }); });