Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/core/parser-claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,40 @@ describe('parseClaudeSessions', () => {
}
});

// ---- workspace name resolution with characters outside [A-Za-z0-9] ----

it.each(['my_home_dir', 'my.app', '.config', 'café'])(
'resolves workspace name when a parent folder is %s', { timeout: 30_000 }, (parentName) => {
// Claude Code replaces every character outside [A-Za-z0-9] with a hyphen,
// so the encoded name only matches readdirSync output if we apply the same rule.
const tmpBase = fs.mkdtempSync(path.join(longTmpDir(), 'claude-enc-'));
const targetDir = path.join(tmpBase, parentName, 'proj');
fs.mkdirSync(targetDir, { recursive: true });

const encodedDirName = targetDir
.replace(/^([a-zA-Z])(?=:)/, d => d.toLowerCase())
.replaceAll(/[^a-zA-Z0-9]/g, '-');

const root = fs.mkdtempSync(path.join(longTmpDir(), 'claude-proj-'));
const projectsDir = path.join(root, 'projects');
const projDir = path.join(projectsDir, encodedDirName);
fs.mkdirSync(projDir, { recursive: true });
fs.writeFileSync(
path.join(projDir, 's.jsonl'),
[makeUser('hello'), makeAssistant('hi')].map(l => JSON.stringify(l)).join('\n'),
);

try {
const result = parseClaudeSessions(projectsDir);
expect(result).toHaveLength(1);
expect(result[0].workspaceName).toBe('proj');
} finally {
fs.rmSync(root, { recursive: true, force: true });
fs.rmSync(tmpBase, { recursive: true, force: true });
}
},
);

// ---- workspace name resolution with spaces ----

it('resolves workspace name when path contains a folder with spaces', { timeout: 30_000 }, () => {
Expand Down
11 changes: 6 additions & 5 deletions src/core/parser-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,19 +452,20 @@ export function findClaudeDirs(): string[] {
}

/** Encode a single filesystem component the way Claude Code does:
* replace whitespace with hyphens. Path separators never appear in a single
* replace every character outside A-Z, a-z and 0-9 with a hyphen (one per
* UTF-16 code unit). Path separators never appear in a single
* component name, and colons are not handled here because Windows component
* names cannot contain them. */
function encodeComponentForMatch(name: string): string {
return name.replace(/\s/g, '-');
return name.replaceAll(/[^a-zA-Z0-9]/g, '-');
}

/**
* Resolve an encoded Claude project directory name back to the real folder name.
*
* Claude encodes workspace paths by replacing `/`, `\`, `:`, and whitespace
* with `-`. For example `c--dev-AI-Engineering-Coach` represents
* `C:\dev\AI-Engineering-Coach`. Because the encoding is lossy (hyphens, spaces,
* Claude encodes workspace paths by replacing every character outside
* A-Z, a-z and 0-9 (one per UTF-16 code unit) with `-`. For example
* `c--dev-AI-Engineering-Coach` represents `C:\dev\AI-Engineering-Coach`. Because the encoding is lossy (hyphens, spaces,
* and path separators all become `-`) we resolve the name by listing each
* directory level and matching entries against the remaining encoded string.
*
Expand Down
Loading