The 'By Project' panel collapsed sibling Claude Code projects to a parent
folder in two cases:
1) resolveCanonicalProjectPath walked up to ANY ancestor .git and used it as
the canonical project. A stray .git in a parent of several projects (a
dotfiles bare repo, an accidental git init) made every sibling resolve to
that parent ('Projects'/'home'). Now only a real linked worktree (.git is a
FILE pointing at <main>/.git/worktrees/<name>) canonicalizes to its main
repo; an ordinary repo or a stray ancestor .git keeps the session's own cwd
as the project. Genuine worktree grouping is preserved.
2) When a Claude session has no cwd metadata, the dir slug was unsanitized by
replacing every '-' with '/', inventing path segments that overview then
split on, so 'Projects-Content-OS' rendered as 'os'. The lossy slug is now
kept intact, and overview only basenames ABSOLUTE paths.
Fixes#493
* fix: fix and improve test isolation and collision with environment
* docs: remove unnecessary comment
* test(env-isolation): clear CODEBURN_FORCE_MACOS_MAJOR and pin TZ
Two env vars read in src/ were not isolated: CODEBURN_FORCE_MACOS_MAJOR
(now cleared so it cannot leak between tests) and TZ (now pinned to UTC,
since clearing it falls back to the OS zone and would shift date buckets
versus a clean CI runner).
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* feat(claude): group Cowork sessions by space name instead of sanitized path
Claude Desktop local-agent-mode (Cowork) sessions were appearing as
separate projects with cryptic sanitized-path names like
`-Users-carlo-Library-Application-Support-Claude-...`. This meant
sessions from the same Cowork space (e.g. PhD_Thesis) showed up as
dozens of unrelated entries.
Changes:
- `discoverSessions()` detects paths inside the desktop sessions dir
(`~/Library/Application Support/Claude/local-agent-mode-sessions`)
and resolves the human-readable space name by reading the sibling
`local_<id>.json` (for spaceId) and `spaces.json` (id → name map)
- `CODEBURN_DESKTOP_SESSIONS_DIR` env var overrides the desktop path
for testability
- Sessions without a spaceId fall back to `userSelectedFolders[0]`
basename, then the session title (instead of the unintelligible slug)
- `isCoworkOutputPath()` in parser.ts prevents the ephemeral
`outputs/` cwd from being used as the canonical project key,
which would have given every session a unique grouping key
- Parse-version bump (`cowork-space-grouping-v1`) invalidates stale
cache so all files re-scan with the new logic on first run
- 5 new integration tests covering spaceId grouping, cross-space
separation, folder fallback, title fallback, and no-metadata fallback
* test: use generic Project1/Project2/SubFolder names in cowork tests
* fix(parser): detect container-mode Cowork sessions by file path, not just cwd
Cowork sessions run in two modes:
- Local-mode: cwd is an ephemeral per-session outputs/ dir inside the
desktop sessions directory — already handled.
- Container-mode: session runs inside Docker, so cwd is a container-local
path like /sessions/<adjective-name> that does not exist on the host.
The JSONL file still lives inside the desktop sessions directory, so we
detect these by also checking the file path.
Rename isCoworkOutputPath → isCoworkSession and check both cwd and filePath.
Without this, container-mode sessions leaked their Docker cwd as the
canonical project path and showed as e.g. "sessions/trusting-inspiring-ri"
instead of grouping under the Cowork space name.
Also add a test covering the container-mode case and use generic names
(Project1, Project2, SubFolder) throughout the Cowork test suite.
* feat(parser): merge same-project sessions across AI providers by path
When a repository has sessions from multiple tools (e.g. both Claude Code
and Codex), parseAllSessions now groups them into a single ProjectSummary
instead of showing duplicate entries.
The previous outer merge keyed on p.project (a string name), so Claude's
"LibricicloAgain" and Codex's "Users-carlo-Desktop-LibricicloAgain" were
treated as different projects despite referring to the same directory.
The root cause is that Codex's sanitizeProject() strips the leading '/'
from cwds, producing "Users/carlo/Desktop/foo" instead of
"/Users/carlo/Desktop/foo". The new crossProviderKey normalises both by
stripping leading slashes and lowercasing before comparison. Non-path
identifiers (like Cowork space names: "PhD_Thesis") have no '/' and fall
back to comparing the project name, so they are unaffected.
* feat(parser): resolve other-provider worktrees before cross-provider merge
Codex (and similar tools) store worktrees under paths like
~/.codex/worktrees/<hash>/Repo. These were not being resolved to their
main-repo path because canonicalizeProviderCallProject only operates on
call.projectPath, which Codex doesn't set — the project is inferred from
the session source slug instead.
Fix: before the cross-provider merge in parseAllSessions, run worktree
resolution on each non-Claude ProjectSummary's projectPath. Restore the
missing leading '/' (Codex strips it via sanitizeProject) before calling
resolveCanonicalProjectPath so the stat() can find the .git file. If the
path is a worktree, replace project name and path with the canonical
main-repo values so the downstream crossProviderKey matches.
* feat(dashboard): show more projects in By Project panel to fill panel height
The panel was hardcoded to 8 projects regardless of the Daily Activity
panel height alongside it. Pass the same `days` value used by Daily Activity
so the two panels stay in sync: 14 for week/6-months views, 31 for
month/30-days, 20 for all-time, and a minimum of 8 for the today view.
* feat(parser): resolve subdirectory sessions to their git repo root
When a cwd like ~/Repo/src/subdir is recorded, it should group with the
rest of the ~/Repo project. Previously resolveCanonicalProjectPath only
checked for a .git entry at the exact cwd, so subdirectory sessions
created separate ProjectSummary entries.
Walk up the directory tree until a .git entry is found. Guard against
foreign paths (a Windows path like C:\… on macOS cannot be walked on
the local filesystem) by only walking paths that look like absolute
paths on the current platform.
This also folds non-Claude provider subdirectory sessions into the parent
repo via resolveOtherProjectWorktrees, which now triggers whenever
canonical.path differs from the input path (not only for git worktrees).
* Address reviewer feedback: deduplicate getDesktopSessionsDir and fix test
Export getDesktopSessionsDir from claude.ts and import it in parser.ts
instead of maintaining an identical copy in both files.
Update the 'merges by sanitized slug' test to reflect the new path-aware
merge behavior: sessions with the same slug but different cwds now stay
separate (crossProviderKey keys by path, not slug), which is correct since
they represent different repos.
Reads the canonical cwd already stored inside Claude session JSONL files and uses it as the project path, then groups sessions by a normalized path key (case + slash insensitive) so Windows projects no longer split into 3+ rows on case/slash variants. Falls back to the legacy slug-derived path when cwd is missing. Closes#217. Supersedes #228 with a fix that preserves the canonical cwd even when mixed with slug-only sessions in the same directory. Original implementation by @ozymandiashh.