Background
#41 (v0.0.29) fixed the root cause that produced duplicate sidebar entries for projects with spaces, dots, or other non-alphanumeric characters in their paths. encode-project-path.js now matches Claude CLI's folder-naming convention, so any folder Switchboard creates from now on will line up with what the CLI writes. The phantom-archive case (folders whose sessions were all archived showing as empty undismissable entries) was also fixed.
That fix prevents new duplicates from being created. It does not reconcile duplicates that already exist on disk from before the fix shipped.
Symptom
Users who added projects via "Add project" before v0.0.29 (for paths containing spaces, dots, etc.) still see two sidebar entries with the same display name. Example state on disk:
~/.claude/projects/-Users-jla-claude-projects-AI Enablement Plan/ ← Switchboard's old encoding (contains seed .jsonl only)
~/.claude/projects/-Users-jla-claude-projects-AI-Enablement-Plan/ ← Claude CLI's encoding (contains real sessions)
Both decode to /Users/jla/claude-projects/AI Enablement Plan, so the sidebar shows two rows with the same name. The stub folder usually shows the lone seed session ("New project") and the real folder shows everything else.
Why the v0.0.29 fix doesn't cover this
buildProjectsFromCache in session-cache.js keys projectMap by folder, not by projectPath. Walking the current code with the disk state above:
- The seed
.jsonl in the old folder has one unarchived user message → it's a valid cached session
- It passes the archive filter at line 195
- Lines 196–198 create a
projectMap entry keyed by the old folder name
- Real sessions from the new folder create a separate entry keyed by the new folder name
- Both entries carry the same
projectPath → two project objects → two sidebar rows
The phantom-archive fix doesn't help because the seed session isn't archived; it's a real (if trivial) session.
Proposed fix — runtime merge by projectPath
In buildProjectsFromCache, after projectMap is fully built (cached rows + empty dirs + active terminals), fold entries that share a projectPath into one:
```javascript
const projectsByPath = new Map();
for (const proj of projectMap.values()) {
const existing = projectsByPath.get(proj.projectPath);
if (existing) existing.sessions.push(...proj.sessions);
else projectsByPath.set(proj.projectPath, proj);
}
// then iterate projectsByPath.values() for sorting/return
```
Properties:
- Non-destructive — both folders stay on disk untouched
- Robust to any future encoding drift
- Handles the active-terminal injection edge case where the terminal's encoded folder name might differ from a pre-existing on-disk folder for the same projectPath
Alternative — one-time migration
A startup migration could scan ~/.claude/projects/, detect folders whose decoded projectPath matches another folder's correctly-encoded name, move sessions into the correct folder, and rmdir the empty stub. This cleans up the disk but adds failure modes (permissions, races with the CLI). Better as a follow-up if leftover empty folders prove to be a problem in practice.
Recommendation
Ship the runtime merge first — it resolves the user-visible duplicate immediately for everyone affected. Consider the disk-level cleanup as a separate later effort.
Background
#41 (v0.0.29) fixed the root cause that produced duplicate sidebar entries for projects with spaces, dots, or other non-alphanumeric characters in their paths.
encode-project-path.jsnow matches Claude CLI's folder-naming convention, so any folder Switchboard creates from now on will line up with what the CLI writes. The phantom-archive case (folders whose sessions were all archived showing as empty undismissable entries) was also fixed.That fix prevents new duplicates from being created. It does not reconcile duplicates that already exist on disk from before the fix shipped.
Symptom
Users who added projects via "Add project" before v0.0.29 (for paths containing spaces, dots, etc.) still see two sidebar entries with the same display name. Example state on disk:
Both decode to
/Users/jla/claude-projects/AI Enablement Plan, so the sidebar shows two rows with the same name. The stub folder usually shows the lone seed session ("New project") and the real folder shows everything else.Why the v0.0.29 fix doesn't cover this
buildProjectsFromCacheinsession-cache.jskeysprojectMapby folder, not by projectPath. Walking the current code with the disk state above:.jsonlin the old folder has one unarchived user message → it's a valid cached sessionprojectMapentry keyed by the old folder nameprojectPath→ two project objects → two sidebar rowsThe phantom-archive fix doesn't help because the seed session isn't archived; it's a real (if trivial) session.
Proposed fix — runtime merge by projectPath
In
buildProjectsFromCache, afterprojectMapis fully built (cached rows + empty dirs + active terminals), fold entries that share aprojectPathinto one:```javascript
const projectsByPath = new Map();
for (const proj of projectMap.values()) {
const existing = projectsByPath.get(proj.projectPath);
if (existing) existing.sessions.push(...proj.sessions);
else projectsByPath.set(proj.projectPath, proj);
}
// then iterate projectsByPath.values() for sorting/return
```
Properties:
Alternative — one-time migration
A startup migration could scan
~/.claude/projects/, detect folders whose decodedprojectPathmatches another folder's correctly-encoded name, move sessions into the correct folder, andrmdirthe empty stub. This cleans up the disk but adds failure modes (permissions, races with the CLI). Better as a follow-up if leftover empty folders prove to be a problem in practice.Recommendation
Ship the runtime merge first — it resolves the user-visible duplicate immediately for everyone affected. Consider the disk-level cleanup as a separate later effort.