diff --git a/src/core/config-health-helpers.test.ts b/src/core/config-health-helpers.test.ts index cb25d71b..e1954a27 100644 --- a/src/core/config-health-helpers.test.ts +++ b/src/core/config-health-helpers.test.ts @@ -329,3 +329,35 @@ describe('readSnippet', () => { expect(snippet.length).toBe(50); }); }); + +describe('resolveWorkspaceRoot', () => { + it('uses a Claude workspace path that is already the project root', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-claude-root-')); + try { + const resolved = resolveWorkspaceRoot('claude--home-me-proj', { id: 'claude--home-me-proj', name: 'proj', path: root }); + expect(resolved).toBe(root); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } + }); + + it('still reads cwd from a session file when the path is a .claude/projects store', () => { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'cfg-claude-store-')); + const projectRoot = path.join(base, 'real-project'); + const store = path.join(base, '.claude', 'projects', '-tmp-real-project'); + fs.mkdirSync(projectRoot, { recursive: true }); + fs.mkdirSync(store, { recursive: true }); + fs.writeFileSync(path.join(store, 's.jsonl'), JSON.stringify({ cwd: projectRoot }) + '\n'); + try { + expect(resolveWorkspaceRoot('claude-x', { id: 'claude-x', name: 'x', path: store })).toBe(projectRoot); + } finally { + fs.rmSync(base, { recursive: true, force: true }); + } + }); + + it('returns null for a Claude workspace whose path no longer exists', () => { + const gone = path.join(os.tmpdir(), 'cfg-claude-missing-' + Date.now()); + expect(resolveWorkspaceRoot('claude-gone', { id: 'claude-gone', name: 'gone', path: gone })).toBeNull(); + }); +}); + diff --git a/src/core/config-health-helpers.ts b/src/core/config-health-helpers.ts index ff2598f9..9d693f38 100644 --- a/src/core/config-health-helpers.ts +++ b/src/core/config-health-helpers.ts @@ -74,14 +74,29 @@ function firstStringProperty(value: Record, ...keys: string[]): export function resolveWorkspaceRoot(id: string, ws: Workspace): string | null { if (id.startsWith('claude-')) { + // `ws.path` is normally the project root already, as it is for the other + // CLI harnesses. Fall back to reading `cwd` out of a session file for + // workspaces whose path still points at ~/.claude/projects/. + if (isProjectRoot(ws.path)) return ws.path; return resolveClaudeRoot(ws.path); } if (id.startsWith('codex-') || id.startsWith('opencode-')) { - return fs.existsSync(ws.path) ? ws.path : null; + return isProjectRoot(ws.path) ? ws.path : null; } return resolveVsCodeRoot(ws.path) ?? resolveCLIRoot(ws.path); } +/** True when the path is an existing directory that is not a Claude session + * store (those hold .jsonl transcripts, not the user's project files). */ +function isProjectRoot(p: string): boolean { + try { + if (!fs.statSync(p).isDirectory()) return false; + } catch { + return false; + } + return path.basename(path.dirname(p)) !== 'projects' || !path.dirname(p).includes('.claude'); +} + function resolveVsCodeRoot(storagePath: string): string | null { const wsJson = path.join(storagePath, 'workspace.json'); const data = readJsonFile(wsJson);