From d0fdbfd655141a3d4d903e6ad374d272acc5cdbb Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:37:43 -0600 Subject: [PATCH] fix(desktop): show repo-root-only sessions in project drill-in (#94552) --- .../sidebar/projects/workspace-groups.test.ts | 54 +++++++++++++++++++ .../chat/sidebar/projects/workspace-groups.ts | 31 +++++++---- 2 files changed, 76 insertions(+), 9 deletions(-) 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 c81f1d72b1..34041664e6 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 @@ -629,6 +629,60 @@ describe('overlayLiveLanes', () => { expect(overlaid.sessionCount).toBe(1) }) + it('keeps cwd-less repo sessions visible in both the overview and project drill-in', () => { + const project = projectNode({ + id: '/www/app', + isAuto: true, + repos: [ + { + id: '/www/app', + label: 'app', + path: '/www/app', + sessionCount: 0, + groups: [lane({ id: '/www/app::branch::main', label: 'main', isMain: true, path: '/www/app' })] + } + ] + }) + + const live = makeCwdSession(null, { id: 'fresh', git_branch: 'main', git_repo_root: '/www/app' }) + + expect(overlayLivePreviews([project], [live], [], 3)['/www/app'].map(session => session.id)).toEqual(['fresh']) + + const overlaid = overlayLiveLanes(project, [live]) + + expect(overlaid.repos[0].groups.flatMap(group => group.sessions.map(session => session.id))).toEqual(['fresh']) + expect(overlaid.sessionCount).toBe(1) + }) + + it('places a cwd-less repo session only in its exact repo subtree', () => { + const project = projectNode({ + id: '/www', + repos: [ + { + id: '/www', + label: 'www', + path: '/www', + sessionCount: 0, + groups: [lane({ id: '/www::branch::main', label: 'main', isMain: true, path: '/www' })] + }, + { + id: '/www/app', + label: 'app', + path: '/www/app', + sessionCount: 0, + groups: [lane({ id: '/www/app::branch::main', label: 'main', isMain: true, path: '/www/app' })] + } + ] + }) + + const live = makeCwdSession(null, { id: 'fresh', git_branch: 'main', git_repo_root: '/www/app' }) + const overlaid = overlayLiveLanes(project, [live]) + + expect(overlaid.repos[0].groups.flatMap(group => group.sessions)).toEqual([]) + expect(overlaid.repos[1].groups.flatMap(group => group.sessions.map(session => session.id))).toEqual(['fresh']) + expect(overlaid.sessionCount).toBe(1) + }) + it('injects a session created in a fresh worktree into that worktree lane (no git_repo_root yet)', () => { // The brand-new session row has only a cwd — no git_repo_root. The entered // project knows its repo root, so the worktree session still lands in its 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 9c30fca8c6..184561dd9a 100644 --- a/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts +++ b/apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts @@ -451,21 +451,34 @@ export function sessionProjectColor(session: SessionInfo, projects: ProjectInfo[ const upsertSession = (rows: SessionInfo[], session: SessionInfo): SessionInfo[] => [session, ...rows.filter(row => row.id !== session.id)].sort((a, b) => sessionRecency(b) - sessionRecency(a)) +/** A live row's placement path, with an exact repo-root fallback when cwd is absent. */ +function livePathForRepo(repoRoot: string, session: SessionInfo): string { + const cwd = (session.cwd || '').trim() + + if (cwd) { + return cwd + } + + const persistedRoot = (session.git_repo_root || '').trim() + + return persistedRoot && pathKey(persistedRoot) === pathKey(repoRoot) ? persistedRoot : '' +} + /** - * The lane a live session belongs to WITHIN a known repo root, by path — the - * entered project already knows its repo roots, so we don't need the session's - * (often-unset, on a fresh row) git_repo_root. Mirrors the backend's lane ids: + * The lane a live session belongs to WITHIN a known repo root, by path. A fresh + * row normally uses cwd; older/imported rows can carry only git_repo_root, which + * still identifies the main checkout exactly. Mirrors the backend's lane ids: * main checkout -> branch lane, `.worktrees/t_` -> kanban, any other * `.worktrees/` -> that worktree's own lane. */ function liveLaneForRepo(repoRoot: string, session: SessionInfo): null | SidebarSessionGroup { - const cwd = (session.cwd || '').trim() + const sessionPath = livePathForRepo(repoRoot, session) - if (!cwd || !isPathUnder(repoRoot, cwd)) { + if (!sessionPath || !isPathUnder(repoRoot, sessionPath)) { return null } - const wt = cwd.match(/^(.*[/\\]\.worktrees)[/\\]([^/\\]+)/) + const wt = sessionPath.match(/^(.*[/\\]\.worktrees)[/\\]([^/\\]+)/) if (wt) { const [worktreeRoot, worktreesDir, slug] = [wt[0], wt[1], wt[2]] @@ -516,9 +529,9 @@ export function overlayRepoLanes( }) for (const session of live) { - const cwd = (session.cwd || '').trim() + const sessionPath = livePathForRepo(repo.path ?? '', session) - if (removed.has(session.id) || !cwd) { + if (removed.has(session.id) || !sessionPath) { continue } @@ -533,7 +546,7 @@ export function overlayRepoLanes( for (const g of lanes) { const lanePath = normalizePath(g.path) - if (!lanePath || pathKey(lanePath) === repoRootKey || !isPathUnder(lanePath, cwd)) { + if (!lanePath || pathKey(lanePath) === repoRootKey || !isPathUnder(lanePath, sessionPath)) { continue }