diff --git a/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.test.ts b/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.test.ts index 6548d4a3b6..931a4dad5c 100644 --- a/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.test.ts +++ b/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.test.ts @@ -901,6 +901,39 @@ describe('overlayLiveLanes', () => { expect(overlayLiveLanes(home, [makeSession('/www/app', { id: 'fresh' })])).toBe(home) }) + + it('evicts a session from the main lane when the live overlay places it into a worktree lane', () => { + // Session was in main when the backend tree was captured, but the live + // $sessions cache now has it under a worktree cwd. The overlay must place + // it ONLY in the worktree lane — not both. + const session = makeSession('/www/app/.worktrees/feature', { id: 'moved', git_branch: 'feature' }) + + const project = projectNode({ + id: '/www/app', + repos: [ + { + id: '/www/app', + label: 'app', + path: '/www/app', + sessionCount: 1, + groups: [ + lane({ id: '/www/app::branch::main', label: 'main', isMain: true, path: '/www/app', sessions: [session] }), + lane({ id: '/www/app/.worktrees/feature', label: 'feature', path: '/www/app/.worktrees/feature', sessions: [] }) + ] + } + ] + }) + + const overlaid = overlayLiveLanes(project, [session]) + const mainLane = overlaid.repos[0].groups.find(g => g.isMain) + const featureLane = overlaid.repos[0].groups.find(g => g.path === '/www/app/.worktrees/feature') + + // Session must NOT appear in the main lane + expect(mainLane?.sessions ?? []).toHaveLength(0) + // Session must appear only in the worktree lane + expect(featureLane?.sessions.map(s => s.id)).toEqual(['moved']) + expect(overlaid.sessionCount).toBe(1) + }) }) describe('overlayLivePreviews', () => { diff --git a/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts b/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts index 756600d495..52370653ba 100644 --- a/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts +++ b/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts @@ -586,6 +586,22 @@ export function overlayRepoLanes( } } + // Evict the session from any OTHER lane the backend snapshot may have + // placed it in (e.g. a turn that moved the session's cwd from main to a + // new worktree — the overlay places it into the worktree lane, but without + // this eviction the stale main-lane entry persists and the session appears + // under both groups until the next backend tree refresh). + for (const g of lanes) { + if (g !== lane) { + const idx = g.sessions.findIndex(s => s.id === session.id) + + if (idx >= 0) { + g.sessions = [...g.sessions.slice(0, idx), ...g.sessions.slice(idx + 1)] + changed = true + } + } + } + lane.sessions = upsertSession(lane.sessions, session) changed = true }