From 7ff9d7db91830ada55da309be669a618571efbca Mon Sep 17 00:00:00 2001 From: Dan Bennett Date: Sat, 8 Aug 2026 14:33:30 -0500 Subject: [PATCH] fix(desktop): match a project to its cwd across Windows path spellings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project ownership compared paths literally, so a nested cwd failed to match its project whenever the separator or drive-letter case differed — which on Windows is routine. Normalise both sides for comparison only, folding case for drive and UNC paths. --- apps/desktop/src/store/projects.test.ts | 8 ++++++++ apps/desktop/src/store/projects.ts | 22 ++++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/store/projects.test.ts b/apps/desktop/src/store/projects.test.ts index cd85c6e185..5071485195 100644 --- a/apps/desktop/src/store/projects.test.ts +++ b/apps/desktop/src/store/projects.test.ts @@ -22,6 +22,7 @@ import { exitProjectScope, openProjectCreate, pickProjectFolder, + projectIdForCwd, projectNameForCwd, refreshProjects, refreshProjectTree, @@ -241,6 +242,13 @@ describe('projectNameForCwd', () => { expect(projectNameForCwd('/elsewhere/mono-feature/src')).toBe('Monorepo') }) + it('matches nested Windows paths across separator and case differences', () => { + $projectTree.set([treeNode({ id: 'p_win', label: 'Windows app', path: 'C:\\Repos\\App' })]) + + expect(projectIdForCwd('c:/repos/app/src')).toBe('p_win') + expect(projectNameForCwd('c:/repos/app/src')).toBe('Windows app') + }) + it('ignores auto-projects and the No-project bucket (no named identity)', () => { $projectTree.set([ treeNode({ id: '/repos/loose', label: 'loose', path: '/repos/loose', isAuto: true }), diff --git a/apps/desktop/src/store/projects.ts b/apps/desktop/src/store/projects.ts index 4471673c3d..8629fa80a6 100644 --- a/apps/desktop/src/store/projects.ts +++ b/apps/desktop/src/store/projects.ts @@ -229,8 +229,26 @@ export function resolveNewSessionCwd(): string { return workspaceCwdForNewSession() } -const underPath = (parent: string, child: string): boolean => - child === parent || child.startsWith(parent.endsWith('/') ? parent : `${parent}/`) +// Comparison-only path spelling: accept either separator everywhere, trim a +// trailing separator, and fold case for Windows drive/UNC paths. The backend +// uses the same host-aware identity rule when assigning sessions to projects. +const comparisonPath = (path: string): string => { + const raw = path.trim() + const windows = /^[A-Za-z]:[/\\]/.test(raw) || raw.startsWith('\\') || raw.startsWith('//') + const normalized = raw.replace(/\\/g, '/').replace(/\/+$/, '') + + return windows ? normalized.toLowerCase() : normalized +} + +const underPath = (parent: string, child: string): boolean => { + const normalizedParent = comparisonPath(parent) + const normalizedChild = comparisonPath(child) + + return ( + normalizedChild === normalizedParent || + normalizedChild.startsWith(normalizedParent.endsWith('/') ? normalizedParent : `${normalizedParent}/`) + ) +} // The project (explicit or auto) that owns `cwd`, by longest path match across // the live tree. Null when no project covers it (it'll surface as a fresh