a plugin can ask for its own chats back
a plugin's chats run in $DATA_PATH/<email>/plugins/<appName>/chats, built server-side and deliberately never sent to the browser. which left the plugin able to START conversations in its own directory and with no way to LIST them — the path is the one thing it cannot name. so /chat/sessions takes ?context=plugin&contextId=<appName> and resolves it the same way the turn does. the four call sites already shared cwdOf; it takes the identity now rather than just the home. the plain ?cwd= form stays for /chat, where the user picks a directory and knows what they picked. it is not a hole either way: listClaudeSessions reads that identity's own ~/.claude/projects, so naming another directory shows you your own conversations elsewhere, never anybody else's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { getPluginChatsDir } from '@@/data-path';
|
||||
import type { Context } from 'hono';
|
||||
import { emitToSession, sessionKeyForTranscript } from './websocket';
|
||||
import { pauseBackgroundTask, restartBackgroundTask } from './task-restart';
|
||||
@@ -108,7 +109,24 @@ export const chatRouter = createRouter();
|
||||
// caller's own home. Claude groups sessions by cwd, so this selects which project group we read.
|
||||
// OpenCode runs on one fixed serve, but each session records the directory its turn ran in, so cwd
|
||||
// selects there too.
|
||||
const cwdOf = (ctx: Context, home: string): string => ctx.req.query('cwd')?.trim() || home;
|
||||
/**
|
||||
* Which directory a listing is about.
|
||||
*
|
||||
* `?context=plugin&contextId=<appName>` resolves server-side, and it exists because the client CANNOT
|
||||
* name this one: a plugin's chats run in `$DATA_PATH/<email>/plugins/<appName>/chats`, built by
|
||||
* `resolveChatCwd` and deliberately never sent to the browser. Without this, a plugin can start chats in
|
||||
* its own directory and then has no way to ask for them back.
|
||||
*
|
||||
* The plain `?cwd=` form stays for `/chat`, where the user picks a directory and knows what they picked.
|
||||
* It is not a hole — `listClaudeSessions` reads that identity's own `~/.claude/projects`, so naming
|
||||
* another directory shows you your own conversations elsewhere, never anybody else's.
|
||||
*/
|
||||
const cwdOf = (ctx: Context, who: { home: string; email: string }): string => {
|
||||
const context = ctx.req.query('context');
|
||||
const contextId = ctx.req.query('contextId')?.trim();
|
||||
if (context === 'plugin' && contextId) return getPluginChatsDir(who.email, contextId);
|
||||
return ctx.req.query('cwd')?.trim() || who.home;
|
||||
};
|
||||
|
||||
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
|
||||
chatRouter.get('/pwds', async (ctx) => {
|
||||
@@ -121,7 +139,7 @@ chatRouter.get('/pwds', async (ctx) => {
|
||||
chatRouter.get('/sessions', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const who = await chatIdentity(user);
|
||||
const cwd = cwdOf(ctx, who.home);
|
||||
const cwd = cwdOf(ctx, who);
|
||||
|
||||
// `?all=1` — every directory at once, for the list's Latest tab. A separate parameter rather than a
|
||||
// separate route because the response shape is identical and every consumer downstream (rename, delete,
|
||||
@@ -205,7 +223,7 @@ chatRouter.get('/sessions/:id', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const who = await chatIdentity(user);
|
||||
const id = ctx.req.param('id');
|
||||
const cwd = cwdOf(ctx, who.home);
|
||||
const cwd = cwdOf(ctx, who);
|
||||
// Fall back to a by-id scan when the (default) cwd doesn't hold it — a fresh /chat/<id> deep-link/refresh
|
||||
// doesn't know the session's cwd. The returned detail carries the real cwd for the client to scope the UI.
|
||||
// 404 rather than 403 on the OpenCode branch: a non-owner has no way to tell a session they may not read
|
||||
@@ -418,7 +436,7 @@ chatRouter.get('/live', async (ctx) => {
|
||||
chatRouter.delete('/sessions/:id', async (ctx) => {
|
||||
const who = await chatIdentity(ctx.get('user'));
|
||||
const id = ctx.req.param('id');
|
||||
const cwd = cwdOf(ctx, who.home);
|
||||
const cwd = cwdOf(ctx, who);
|
||||
if (isOpenCodeSessionId(id) && !who.isOwner) return ctx.text('Not found', 404);
|
||||
if (isOpenCodeSessionId(id)) {
|
||||
if (!(await deleteOpenCodeSession(id))) return ctx.text('Not found', 404);
|
||||
@@ -457,7 +475,7 @@ chatRouter.delete('/sessions/:id', async (ctx) => {
|
||||
chatRouter.patch('/sessions/:id/title', async (ctx) => {
|
||||
const who = await chatIdentity(ctx.get('user'));
|
||||
const id = ctx.req.param('id');
|
||||
const cwd = cwdOf(ctx, who.home);
|
||||
const cwd = cwdOf(ctx, who);
|
||||
const { title } = await ctx.req.json<{ title?: string }>();
|
||||
if (!title?.trim()) return ctx.text('title is required', 400);
|
||||
if (isOpenCodeSessionId(id) && !who.isOwner) return ctx.text('Not found', 404);
|
||||
|
||||
Reference in New Issue
Block a user