chat urls: a group is a path, a session decides its own directory
the chat group moves from ?cwd= to a path suffix behind a g/ discriminator (/chat/g/home/me/project), and a session url carries no group at all. the real fix is not the spelling. a session's working directory was read back off the query string to decide where the agent executes, so the address bar was the authority on where code runs. a pasted or refreshed /chat/<id> arrives with no ?cwd= at all, so a turn sent before the resolve landed ran in the default general_chat_sessions dir instead of the project; and a hand-edited ?cwd= could name a group the session doesn't belong to, with nothing to reconcile them. loadClaudeSessionById already resolves a session's cwd from the id alone, so the id is the only source of truth there. it now travels on SelectedSession.cwd, which is what the composer reads. the url can no longer contradict it. the vocabulary lives in apps/ChatHistory/chat-routes.ts so a link built in a panel and one built in a screen cannot drift. also: startAgentRun no longer returns a literal chatUrl — it returns cwd and AgentRunnerModal builds the link, so the server holds no copy of the frontend url shape. and the post-turn permalink strips a stale ?cwd= instead of carrying it forward onto the new session's url. walkthrough doc gains item 13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -117,10 +117,14 @@ type StartAgentRunParams = {
|
||||
export type StartAgentRunResult = {
|
||||
sessionKey: string;
|
||||
dirName: string;
|
||||
/**
|
||||
* The directory the run executes in — which is also its project group in /chat, so the caller can
|
||||
* build the link to it. This used to also return a ready-made `chatUrl`, which meant the server held
|
||||
* an opinion about frontend URL shape and drifted the moment that shape changed. `cwd` is the fact;
|
||||
* the URL is the frontend's business (`chatListPath`).
|
||||
*/
|
||||
cwd: string;
|
||||
model: string;
|
||||
/** Where to look at this run: the agent's own project group in /chat, newest session on top. */
|
||||
chatUrl: string;
|
||||
};
|
||||
|
||||
export async function startAgentRun(params: StartAgentRunParams): Promise<StartAgentRunResult> {
|
||||
@@ -204,6 +208,5 @@ export async function startAgentRun(params: StartAgentRunParams): Promise<StartA
|
||||
dirName: agent.dirName,
|
||||
cwd,
|
||||
model,
|
||||
chatUrl: `/chat?cwd=${encodeURIComponent(cwd)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,10 +255,35 @@ export function loadClaudeSessionById(email: string, sessionId: string): ClaudeS
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The transcript file for a session: in the named group if it is there, otherwise wherever it actually
|
||||
* is. The caller's cwd is a hint, not an authority — a session's group is a property of the session,
|
||||
* and the two disagree routinely (the list is showing one group while you act on a row from another,
|
||||
* or a deep link hasn't resolved its group yet). Reads have always fallen back like this; writes did
|
||||
* not, so delete and rename returned "not found" for a session that was plainly on screen.
|
||||
*/
|
||||
function findTranscript(email: string, cwd: string, sessionId: string): string | null {
|
||||
const preferred = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`);
|
||||
if (existsSync(preferred)) return preferred;
|
||||
|
||||
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)) return filePath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Delete a session by removing its transcript file. Returns false if it didn't exist. */
|
||||
export function deleteClaudeSession(email: string, cwd: string, sessionId: string): boolean {
|
||||
const filePath = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`);
|
||||
if (!existsSync(filePath)) return false;
|
||||
const filePath = findTranscript(email, cwd, sessionId);
|
||||
if (!filePath) return false;
|
||||
rmSync(filePath);
|
||||
return true;
|
||||
}
|
||||
@@ -269,8 +294,8 @@ export function deleteClaudeSession(email: string, cwd: string, sessionId: strin
|
||||
* timestamp is written so the rename doesn't reorder the list.
|
||||
*/
|
||||
export function renameClaudeSession(email: string, cwd: string, sessionId: string, title: string): boolean {
|
||||
const filePath = join(claudeProjectsDir(email), projectSlug(cwd), `${sessionId}.jsonl`);
|
||||
if (!existsSync(filePath)) return false;
|
||||
const filePath = findTranscript(email, cwd, sessionId);
|
||||
if (!filePath) return false;
|
||||
|
||||
// Attach the summary to the transcript's tip (the last entry carrying a uuid).
|
||||
let leafUuid = sessionId;
|
||||
|
||||
Reference in New Issue
Block a user