diff --git a/headroom/learn/plugins/claude.py b/headroom/learn/plugins/claude.py index 4589cb5e3..e7ae19584 100644 --- a/headroom/learn/plugins/claude.py +++ b/headroom/learn/plugins/claude.py @@ -96,6 +96,11 @@ class ClaudeCodePlugin(LearnPlugin, ConversationScanner): if not jsonl_files: continue + session_project_path = self._project_path_from_session_cwd(jsonl_files) + if session_project_path is not None: + project_path = session_project_path + name = _project_display_name(project_path, entry.name) + projects.append( ProjectInfo( name=name, @@ -108,6 +113,28 @@ class ClaudeCodePlugin(LearnPlugin, ConversationScanner): return projects + @staticmethod + def _project_path_from_session_cwd(jsonl_files: list[Path]) -> Path | None: + for jsonl_path in sorted(jsonl_files): + try: + with open(jsonl_path, encoding="utf-8", errors="replace") as f: + for line in f: + if not line.strip(): + continue + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + cwd = event.get("cwd") + if isinstance(cwd, str) and cwd: + project_path = Path(cwd) + if project_path.exists(): + return project_path + except (OSError, UnicodeDecodeError): + continue + return None + + def scan_project( self, project: ProjectInfo, max_workers: int = 1, include_subagents: bool = True ) -> list[SessionData]: diff --git a/tests/test_learn/test_scanner.py b/tests/test_learn/test_scanner.py index 49560241a..15b7aa718 100644 --- a/tests/test_learn/test_scanner.py +++ b/tests/test_learn/test_scanner.py @@ -8,6 +8,7 @@ making it impossible to reconstruct names formed from three or more tokens. from __future__ import annotations +import json from collections.abc import Generator from pathlib import Path from uuid import uuid4 @@ -417,6 +418,25 @@ class TestDecodeProjectPath: assert projects[0].name == "work" assert str(projects[0].project_path).startswith("C:") + def test_discover_project_prefers_session_cwd_over_ambiguous_folder_name( + self, tmp_path: Path + ) -> None: + nested = tmp_path / "vibe" / "remote" + hyphenated = tmp_path / "vibe-remote" + nested.mkdir(parents=True) + hyphenated.mkdir() + + claude_dir = tmp_path / ".claude" + project_dir = claude_dir / "projects" / "C--Users-rod-work-vibe-remote" + project_dir.mkdir(parents=True) + (project_dir / "session.jsonl").write_text(json.dumps({"cwd": str(hyphenated)}) + "\n") + + projects = ClaudeCodeScanner(claude_dir=claude_dir).discover_projects() + + assert len(projects) == 1 + assert projects[0].name == "vibe-remote" + assert projects[0].project_path == hyphenated + def test_windows_double_dash_encoding_decodes(self) -> None: """Real Claude Code encoding has no leading dash: C:\\Users\\x → C--Users-x (#1849).