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 type { Context } from 'hono';
|
||||||
import { emitToSession, sessionKeyForTranscript } from './websocket';
|
import { emitToSession, sessionKeyForTranscript } from './websocket';
|
||||||
import { pauseBackgroundTask, restartBackgroundTask } from './task-restart';
|
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.
|
// 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
|
// OpenCode runs on one fixed serve, but each session records the directory its turn ran in, so cwd
|
||||||
// selects there too.
|
// 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.
|
// GET /chat/pwds — the default /chat dir plus every directory that already has Claude sessions.
|
||||||
chatRouter.get('/pwds', async (ctx) => {
|
chatRouter.get('/pwds', async (ctx) => {
|
||||||
@@ -121,7 +139,7 @@ chatRouter.get('/pwds', async (ctx) => {
|
|||||||
chatRouter.get('/sessions', async (ctx) => {
|
chatRouter.get('/sessions', async (ctx) => {
|
||||||
const user = ctx.get('user');
|
const user = ctx.get('user');
|
||||||
const who = await chatIdentity(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
|
// `?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,
|
// 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 user = ctx.get('user');
|
||||||
const who = await chatIdentity(user);
|
const who = await chatIdentity(user);
|
||||||
const id = ctx.req.param('id');
|
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
|
// 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.
|
// 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
|
// 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) => {
|
chatRouter.delete('/sessions/:id', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
const id = ctx.req.param('id');
|
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) && !who.isOwner) return ctx.text('Not found', 404);
|
||||||
if (isOpenCodeSessionId(id)) {
|
if (isOpenCodeSessionId(id)) {
|
||||||
if (!(await deleteOpenCodeSession(id))) return ctx.text('Not found', 404);
|
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) => {
|
chatRouter.patch('/sessions/:id/title', async (ctx) => {
|
||||||
const who = await chatIdentity(ctx.get('user'));
|
const who = await chatIdentity(ctx.get('user'));
|
||||||
const id = ctx.req.param('id');
|
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 }>();
|
const { title } = await ctx.req.json<{ title?: string }>();
|
||||||
if (!title?.trim()) return ctx.text('title is required', 400);
|
if (!title?.trim()) return ctx.text('title is required', 400);
|
||||||
if (isOpenCodeSessionId(id) && !who.isOwner) return ctx.text('Not found', 404);
|
if (isOpenCodeSessionId(id) && !who.isOwner) return ctx.text('Not found', 404);
|
||||||
|
|||||||
Reference in New Issue
Block a user