chat: pwd selector — switch the /chat working directory

The Sessions panel header gets a pwd selector: the default claude_sessions dir,
plus every directory that already has Claude sessions (auto-discovered by reading
the real cwd back from ~/.claude/projects), plus free-text entry. Switching pwd
refetches the list for that cwd's Claude project group and runs New Chat / resume
in it. Backend: GET /chat/pwds + a ?cwd= param on the session ops; the WS handler
honors a chosen cwd for /chat (default claude_sessions). Browse-modal picker next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-24 12:41:49 +00:00
co-authored by Claude Opus 4.8
parent 420aa08783
commit 04bde70ed3
8 changed files with 237 additions and 36 deletions
+61 -1
View File
@@ -1,4 +1,4 @@
import { readdirSync, readFileSync, existsSync, statSync, mkdirSync, rmSync, appendFileSync } from 'node:fs';
import { readdirSync, readFileSync, existsSync, statSync, mkdirSync, rmSync, appendFileSync, openSync, readSync, closeSync } from 'node:fs';
import { join } from 'node:path';
import { DATA_PATH } from '../../data-path';
@@ -232,6 +232,66 @@ export function renameClaudeSession(email: string, cwd: string, sessionId: strin
return true;
}
// ── Working directories (pwds) ──
// Every dir you've run Claude in shows up as a project group under ~/.claude/projects. We read the
// real `cwd` back from each group's transcripts so the UI can offer "jump to any project's sessions".
/** Read the `cwd` recorded in a transcript, from a bounded head read (cwd appears in early entries). */
function firstCwd(filePath: string): string {
let fd: number | undefined;
try {
fd = openSync(filePath, 'r');
const buf = Buffer.alloc(32768);
const n = readSync(fd, buf, 0, buf.length, 0);
return buf.toString('utf-8', 0, n).match(/"cwd":"([^"]*)"/)?.[1] ?? '';
} catch {
return '';
} finally {
if (fd !== undefined) closeSync(fd);
}
}
export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };
/** All working directories that have Claude sessions, plus the default /chat dir. Newest first. */
export function listClaudePwds(email: string): ClaudePwd[] {
const projectsDir = claudeProjectsDir(email);
const defaultCwd = getClaudeSessionsCwd(email);
const byCwd = new Map<string, { count: number; updatedAt: string }>();
if (existsSync(projectsDir)) {
for (const group of readdirSync(projectsDir)) {
const groupDir = join(projectsDir, group);
let files: string[];
try {
files = readdirSync(groupDir).filter((f) => f.endsWith('.jsonl'));
} catch {
continue; // not a directory
}
if (files.length === 0) continue;
const cwd = firstCwd(join(groupDir, files[0]!));
if (!cwd) continue;
let updatedAt = '';
for (const f of files) {
const m = statSync(join(groupDir, f)).mtime.toISOString();
if (m > updatedAt) updatedAt = m;
}
const prev = byCwd.get(cwd);
byCwd.set(cwd, {
count: (prev?.count ?? 0) + files.length,
updatedAt: prev && prev.updatedAt > updatedAt ? prev.updatedAt : updatedAt,
});
}
}
if (!byCwd.has(defaultCwd)) byCwd.set(defaultCwd, { count: 0, updatedAt: '' });
return [...byCwd.entries()]
.map(([cwd, v]) => ({ cwd, sessionCount: v.count, updatedAt: v.updatedAt, isDefault: cwd === defaultCwd }))
.sort((a, b) => (a.isDefault ? -1 : b.isDefault ? 1 : b.updatedAt.localeCompare(a.updatedAt)));
}
/** List sessions Claude has stored for a given working directory, newest first. */
export function listClaudeSessions(email: string, cwd: string): ClaudeSessionSummary[] {
const dir = join(claudeProjectsDir(email), projectSlug(cwd));