name the live sessions instead of showing their ids
The titles were looked up client-side against the sessions query already in cache, which only covers the group being browsed — so anything running in another directory rendered as a truncated uuid, which is most of them. Resolved server-side now. The agent reports keys and nothing else, so liveSessionTitle finds the transcript by scanning the project slugs, takes the cwd off its own first entry, and hands that to claudeSessionContext — the same path the list uses, so the two agree on naming, /clear chains merged included, rather than offering a second opinion. A null title means no transcript has been written yet. That row says 'Starting…' and is deliberately not a link: pointing at a session you cannot open yet is worse than plainly not being a link. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
renameClaudeSession,
|
||||
loadBackgroundTask,
|
||||
claudeSessionContext,
|
||||
liveSessionTitle,
|
||||
} from './claude-sessions';
|
||||
import {
|
||||
listOpenCodeSessions,
|
||||
@@ -100,8 +101,17 @@ chatRouter.get('/sessions/:id', async (ctx) => {
|
||||
//
|
||||
// `pendingTasks` is background work started but not yet notified — with `isGenerating` it is what the
|
||||
// agent's own idle GC consults, so a caller can tell "busy" from "merely open" the same way it does.
|
||||
// Titles are resolved here rather than in the client: the agent reports session keys and nothing else,
|
||||
// and the client can only name the sessions in the group it happens to be browsing — which is how the
|
||||
// list ended up showing raw ids for anything running elsewhere.
|
||||
chatRouter.get('/live', async (ctx) => {
|
||||
return ctx.json({ sessions: await sidecar.listLiveClaudeSessions() });
|
||||
const email = ctx.get('user').email;
|
||||
const live = await sidecar.listLiveClaudeSessions();
|
||||
const sessions = live.map((session) => {
|
||||
const resolved = isOpenCodeSessionId(session.sessionKey) ? null : liveSessionTitle(email, session.sessionKey);
|
||||
return { ...session, title: resolved?.title ?? null, cwd: resolved?.cwd ?? null };
|
||||
});
|
||||
return ctx.json({ sessions });
|
||||
});
|
||||
|
||||
// DELETE /chat/sessions/:id[?cwd=] — remove a conversation from the owning harness's store. For a
|
||||
|
||||
@@ -841,6 +841,56 @@ export function claudeSessionContext(
|
||||
return part ? { title: part.title, partCount: 1 } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The display title for a session known only by id — what `/chat/live` needs.
|
||||
*
|
||||
* `claudeSessionContext` is the real answer but has to be told the group, and a live session can be in
|
||||
* any of them; the agent reports session keys and nothing else. So find the transcript by scanning the
|
||||
* slugs, take the cwd off its own first entry, and hand that to the normal path — which means the Live
|
||||
* panel shows exactly the title the list shows, `/clear` chains merged and all, rather than a second
|
||||
* opinion about naming.
|
||||
*
|
||||
* Reads the file to answer. That is a few milliseconds against a poll every ten seconds over a handful
|
||||
* of live sessions, so it is not worth a cache yet — but it is worth knowing before this is called from
|
||||
* anywhere hotter.
|
||||
*/
|
||||
export function liveSessionTitle(email: string, sessionId: string): { title: string; cwd: string } | null {
|
||||
const projectsDir = claudeProjectsDir(email);
|
||||
let slugs: string[];
|
||||
try {
|
||||
slugs = readdirSync(projectsDir);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const slug of slugs) {
|
||||
const filePath = join(projectsDir, slug, `${sessionId}.jsonl`);
|
||||
if (!existsSync(filePath)) continue;
|
||||
|
||||
// The cwd is a property of the transcript's entries, so the first one carrying it settles which
|
||||
// group this session belongs to — no need to reverse the slug, which is lossy.
|
||||
let cwd: string | null = null;
|
||||
try {
|
||||
for (const line of readFileSync(filePath, 'utf-8').split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
const entry = JSON.parse(line) as { cwd?: string };
|
||||
if (entry.cwd) {
|
||||
cwd = entry.cwd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!cwd) return null;
|
||||
|
||||
const context = claudeSessionContext(email, cwd, sessionId);
|
||||
return context ? { title: context.title, cwd } : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every transcript that has to go when this conversation is deleted: itself and everything it
|
||||
* continues. The row stands for the whole chain, so deleting it has to mean the whole chain — leaving
|
||||
|
||||
Reference in New Issue
Block a user