fix(path-decode): recover project paths with hyphenated middle segments - #30
Open
playmaker wants to merge 1 commit into
Open
fix(path-decode): recover project paths with hyphenated middle segments#30playmaker wants to merge 1 commit into
playmaker wants to merge 1 commit into
Conversation
playmaker
force-pushed
the
fix/decode-project-path-middle-merge
branch
7 times, most recently
from
July 23, 2026 08:14
e0b17fd to
09c5eca
Compare
The previous try_merge_segments only merged a *prefix* of segments into one dash-joined chunk, leaving the rest as / separators. That misses any path whose dash lives in the middle, e.g. `/srv/repos/<dashed-name>/` decoded to `/srv/repos/<a>/<b>` because no candidate kept the parent as `/` while merging `<a>` and `<b>` with `-`. Rewrite the inner loop to enumerate every contiguous [start, end) segment range and merge only that range with '-'. This subsumes the old behavior (all-merge, prefix-merge, tail-merge) and adds the missing middle-merge case. Tests cover the three surviving configurations plus a no-match case. Co-Authored-By: Claude <noreply@anthropic.com>
playmaker
force-pushed
the
fix/decode-project-path-middle-merge
branch
2 times, most recently
from
July 23, 2026 08:49
09c5eca to
d1c054e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
Lovcode's project list and session display show wrong paths for many projects. Two compounding causes:
Wrong encoder.
encode_project_pathinsrc-tauri/src/app/session_listing.rs:10only handles two characters (/.→--,/→-). Claude Code's real encoder,sanitizePathinsrc/utils/sessionStoragePortable.ts:311, replaces every non-alphanumeric character with-. So any user who pointed Claude Code at a project whose path contains.,_,,:, etc. ended up with two divergent project-id streams that no longer matched each other — Lovcode's writes and Claude Code's writes for the same project landed in different~/.claude/projects/<id>/directories. Subagent research on the claude-code source confirms: every character in/[^a-zA-Z0-9]/gis replaced.Wrong primary display source. Lovcode's
list_projectsresolved each project's display path by callingdecode_project_path(project_id)— a heuristic that tries to invert the encoding. Claude Code never decodes; it readscwddirectly from the firstsession_metaline of each session jsonl, where Claude Code writes the canonical path verbatim. The encoding is fundamentally lossy (a real/path/<a>-<b>and/path/<a>_<b>collapse to the same id), so any heuristic will produce wrong paths for some layouts. The right display source is the jsonl'scwd.The same flaws break worktree layouts (
<dashed-parent>/.worktrees/<dashed-leaf>/) and any project whose path contains non-alphanumeric characters in non-leading positions.Fix
Four coordinated changes:
1.
encode_project_path— match Claude Code's algorithmRewrite the encoder to mirror
sanitizePath:For paths ≤ 200 chars: pure regex replacement. For longer paths: truncate and append a djb2 base-36 hash. Claude Code picks
Bun.hash(wyhash) under Bun orsimpleHash(djb2) under Node; Lovcode only runs in the desktop Tauri app (no Bun), so we always use djb2, which matches whatfindProjectDirin Claude Code falls back to for cross-runtime lookups.2.
list_projects— read cwd from jsonl as primary pathFor each project_dir, find the newest session jsonl (by mtime), read its head with
read_session_head, and usehead.cwdas the display path. Fall back todecode_project_pathonly when no parseable session exists yet (rare edge case: a project dir created but never written to).This is the same approach Claude Code uses internally (
parseSessionInfoFromLiteinsrc/utils/listSessionsImpl.ts:127-128), and it's lossless because Claude Code writes the canonical cwd verbatim into every session's first line.3. Project-level cwd fallback in
compute_all_sessionsandcollect_lightweight_sessions_snapshotA second related issue surfaced after (1) and (2) shipped: some sessions have no
cwdline in their jsonl head — typically tail-only snapshots that Claude Code appends at session start (justlast-prompt/mode/permission-mode, a few hundred bytes, no user-message line). For these sessions,read_session_head(...).cwdisNone, and the previous code fell back to the lossydecode_project_pathheuristic.The fix: pre-scan every
project_dirand build aHashMap<project_id, project_cwd>by reading the cwd from any jsonl in that project_dir that has one (not just the newest — if the newest happens to be a cwd-less tail snapshot, we'd still find cwd in an earlier fuller session). Bothcompute_all_sessions(pass1/pass2 fallback) andcollect_lightweight_sessions_snapshotconsult this map.The fallback chain is now:
head.cwd(per-session, read from this jsonl's head)project_cwd_map[project_id](from any other session under the same project_id)decode_project_path(project_id)(the lossy heuristic, last resort)Sessions within a project_dir all share the same cwd in Claude Code's encoding, so (2) is correct whenever (1) fails.
4.
try_merge_segments— keep 1-block as a fallback, drop the speculative 2-block extensionWith cwd-from-jsonl as primary, the heuristic only runs when no jsonl exists yet. The 2-block extension that we initially added (to cover worktree layouts like
<dashed-parent>/.worktrees/<dashed-leaf>/) is no longer needed — those cases are now resolved by reading the jsonl. The function reverts to enumerating one merge block, which is sufficient for the rare fallback case.5.
SESSIONS_CACHE_VERSIONbumped 6 → 7 (one above main)~/.lovstudio/lovcode/sessions-cache.jsonpersists a fully-decodedSessionper jsonl file, includingproject_path. The old cache was built with the broken decoder, so even after upgrading the binary the UI keeps showing the wrong paths until each cached jsonl file is touched (size/mtime change) and re-read.Bumping the version makes
load_sessions_cachesilently discard v6 entries on next launch, so the upgraded binary repopulates the cache using the fixed decoder + cwd-from-jsonl.Tests
Ten unit tests across
app::session_listing::testsandapp::session_cache::testsagainst real temp directories:try_merge_segments_recovers_middle_merge— 1-block middle case (<parent>/<name>)try_merge_segments_recovers_prefix_merge— old prefix-merge preservedtry_merge_segments_recovers_all_merged— old "all merged" preservedtry_merge_segments_returns_none_when_no_match— non-matching path returnsNoneencode_project_path_replaces_all_non_alphanumeric— matches Claude Code's regex exactly (covers/,.,_, etc.)encode_project_path_truncates_long_paths_with_hash— >200-char paths get truncated + hashencode_project_path_then_decode_with_heuristic_finds_real_path— end-to-end encode → decode round-tripproject_cwd_map_picks_cwd_from_any_session_in_project— even when the newest jsonl is a cwd-less tail snapshot, the map finds cwd from an earlier fuller sessiontail_only_session_falls_back_to_project_cwd— end-to-end: head-less session falls back to project-level cwd via the mapAll ten pass:
cargo test --lib→ 10 passed.Upgrade instructions
After pulling this change, on first launch the existing
sessions-cache.jsonis discarded automatically (version mismatch). To force it manually:Then restart the app. Each project path is re-decoded using the fixed algorithm, and projects with at least one session will display the canonical cwd from the jsonl directly.
Limitations
Claude Code's encoding is fundamentally lossy:
_and-are not distinguished, so e.g./path/<a>-<b>and/path/<a>_<b>would both encode to the same project id. With cwd-from-jsonl as primary, this ambiguity is moot for projects that have at least one session — Claude Code wrote the real path into the jsonl. The lossy heuristic only matters for project dirs without parseable sessions, which is rare.A canonical-path lookup table (mapping project_id → real path) would be the proper long-term fix for the empty-project edge case; out of scope here.