start a commander from a plugin chat, without knowing where anything lives
pointing a commander at a mailbox meant typing its path, which meant a file
browser and an account allowed to walk the disk. only the owner has that — so
"watch my email and tell me when X" was a super-user feature, in a product whose
whole point is that it should not be.
every plugin chat already mounts `ChatHeader` with a `chatContext`, and that
context IS the address: the server turns `{context:'email', contextId:'me@…'}`
into the mailbox's own directory, beside its `emails.db`. so one button on that
header reaches every plugin chat there will ever be, and a new plugin gets it by
declaring the context it already had to declare to have a chat at all. that is
what makes this common to all those chats rather than copied into each.
THE CLIENT SENDS NO PATH, because it has none. a plugin's chats live under
$DATA_PATH/<email>/plugins/<app>/chats and an email chat beside its own database;
both are built server-side and deliberately never sent to the browser. the
resolver moved out of chat.ts into `chat-context.ts` so command-hq uses the SAME
mapping — two copies would be two chances for a commander to watch a directory
its own chat has never been in.
it NAVIGATES rather than creating on the spot. a commander with no first turn has
no transcript, and the grid filters those rows out — so "create it silently"
makes something the person cannot then find. the setup screen is also where the
first turn happens, which is where the job actually gets settled. the button
carries the context there, pre-fills the name, and leaves one button to press.
the reference is shown as what it WATCHES rather than as a path, because there is
no path to show.
caught by testing rather than by reading: `resolveChatContextDir` reads the query
string, and command-hq takes its context in a JSON body — so the first version
resolved every commander to the owner's home. that failure is invisible, since a
commander watching the wrong directory just answers about the wrong thing
forever. the value-taking `chatContextDir` is what command-hq calls.
verified live: context 'email' resolves to email_accounts, plugin/proxy-manager
to its own chats directory.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
type ClaudeSessionSummary,
|
||||
} from '../chat/claude-sessions';
|
||||
import { collectRunningTasks, startClaudeTurn, COMMANDER_DEFAULT_MODEL } from '../chat/websocket';
|
||||
import { chatContextDir } from '../chat/chat-context';
|
||||
import {
|
||||
listCommandHqCategories,
|
||||
createCommandHqCategory,
|
||||
@@ -527,13 +528,33 @@ commandHqRouter.get('/commanders/:sessionId/fires', async (ctx) => {
|
||||
commandHqRouter.post('/commanders', async (ctx) => {
|
||||
const user = ctx.get('user');
|
||||
const who = await commandHqIdentity(user);
|
||||
const body = await ctx.req
|
||||
.json<{ name?: string; referencePath?: string; categoryId?: number | null }>()
|
||||
.catch(() => ({}) as { name?: string; referencePath?: string; categoryId?: number | null });
|
||||
type Body = {
|
||||
name?: string;
|
||||
referencePath?: string;
|
||||
categoryId?: number | null;
|
||||
/** A chat context to watch, instead of a path — see below. */
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
};
|
||||
const body = await ctx.req.json<Body>().catch(() => ({}) as Body);
|
||||
const name = body.name?.trim();
|
||||
const referencePath = body.referencePath?.trim();
|
||||
if (!name) throw errors.BAD_REQUEST('name is required');
|
||||
if (!referencePath) throw errors.BAD_REQUEST('referencePath is required');
|
||||
|
||||
/*
|
||||
A commander started FROM a chat watches the directory that chat runs in.
|
||||
|
||||
The client sends the context it already has — `{context:'email', contextId:'me@…'}` — and never a
|
||||
path, because it cannot know one: a plugin's chats live under `$DATA_PATH/<email>/plugins/<app>/chats`
|
||||
and an email chat beside its own `emails.db`, both built server-side and deliberately not sent to the
|
||||
browser. `resolveChatContextDir` is the same function the chat router uses, so a commander cannot end
|
||||
up watching a directory its own chat has never been in.
|
||||
|
||||
This is also what makes the feature reachable by somebody who is not a super user. Pointing a
|
||||
commander at a mailbox previously meant knowing its path and typing it, which meant a file browser
|
||||
and an account allowed to walk the disk. The button asks for neither.
|
||||
*/
|
||||
const referencePath = body.context ? chatContextDir(body.context, body.contextId, who) : body.referencePath?.trim();
|
||||
if (!referencePath) throw errors.BAD_REQUEST('referencePath or context is required');
|
||||
|
||||
const row = await createCommander({ userId: user.id, name, referencePath, categoryId: body.categoryId ?? null });
|
||||
const { dir, ownershipError, credentialError } = await createCommanderDirectory(who.email, row.dirName, {
|
||||
|
||||
Reference in New Issue
Block a user