a plugin declares its chat directory; email stops being hardcoded here

ChatPanelWrapper had one plugin's screen id compiled into a shared platform
package: `workspace.id === 'email'` → `{ context: 'email' }`. its own comment
said the general fix was a way for a plugin to DECLARE its chat context, and
that a second hardcoded id would be the signal it was needed. that fix exists
(`declared`), the email plugin now passes it, so the branch is gone.

it also could not fire any more, which is the part worth keeping. a plugin's
workspace key is `screens/plugin/<name>`, so email's id became `plugin/email`
the day it was extracted and the test silently stopped matching. a hardcoded id
does not fail loudly when the thing it names moves — it just stops being true,
and nothing said so for eight days.

two things on the backend half:

resolveEmailCwd now installs the email plugin's chat/CLAUDE.md. an email chat
runs in the account's directory, not the plugin's chats directory, so the
`plugin` branch never reaches it — the brief follows the chat rather than the
chat moving to the brief. a second plugin wanting this is the signal that
"which directory does this chat run in" belongs in the manifest.

and it verifies the mailbox exists before joining it. `accountEmail` comes from
the client (the mailbox picker's `?account=`) and this function ends in mkdirSync
— so an unrecognised value did not fail, it CREATED a directory named after
whatever arrived and ran the turn in it. the list's unified view uses the
sentinel `all`, which is the value most likely to show up by accident. the panel
drops it; this is the half that does not depend on the panel remembering to.

docs/claude-md-touchpoints.md updated: the copier now has two callers.

unrelated and NOT from this change: ModelSelector's "an unstarted composer gets
a default" fails on master too — verified by stashing these files and rerunning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-26 18:43:13 +00:00
co-authored by Claude Opus 5
parent d58652bf14
commit be210c3ed4
3 changed files with 77 additions and 51 deletions
+20 -1
View File
@@ -200,9 +200,28 @@ async function resolveEmailCwd(ownerEmail: string, accountEmail?: string): Promi
// Found on disk, never queried. Email is a plugin and the platform may not import its queries; the
// directory layout is already the platform's, so reading it answers the question with no coupling and
// nothing to throw when the plugin is not installed.
const account = accountEmail?.trim() || findEmailAccountName(ownerEmail);
//
// The named mailbox has to EXIST before it is joined. `accountEmail` reaches here from the client — it is
// the mailbox picker's `?account=` — and this function ends in `mkdirSync`, so an unrecognised value does
// not fail, it CREATES a directory named after whatever was sent and runs the turn in it. The list's
// unified view uses the sentinel `all`, which is the value most likely to arrive by accident; the panel
// drops it, and this is the half that does not depend on the panel remembering to.
//
// Falling back to the most recently synced account is the same answer the unified list is mostly showing.
const requested = accountEmail?.trim();
const known = requested && existsSync(join(getEmailAccountsDir(ownerEmail), requested, 'emails.db'));
if (requested && !known) {
console.warn(`[chat] email: no mailbox named ${requested} — using the most recently synced one instead`);
}
const account = (known ? requested : null) ?? findEmailAccountName(ownerEmail);
const dir = account ? join(getEmailAccountsDir(ownerEmail), account) : getEmailAccountsDir(ownerEmail);
mkdirSync(dir, { recursive: true });
// The same brief a plugin's own chats get, written where an email chat actually runs.
//
// `installPluginChatBrief` is not reached by the `plugin` branch below, because an email chat does not
// run in the plugin's chats directory — it runs in the ACCOUNT's, beside `emails.db`, which is the whole
// reason this branch exists. The brief follows the chat rather than the chat moving to the brief.
await installPluginChatBrief('email', dir);
return dir;
}
@@ -203,24 +203,21 @@ export const ChatPanelWrapper = ({
// `contextId` stays the raw workspace key: it is the same string `agent_panels.dashboard_id` holds, and
// a stored address should not be two different strings depending on which feature wrote it. The bare id
// is for deciding, which is what `kind` and `id` are doing above it.
// `'email'` is a PLUGIN's screen id, hardcoded in a shared platform package, and it is the one piece of
// plugin vocabulary the extraction on 2026-08-18 could not remove.
//
// It is the frontend half of a pair: this sends `context: 'email'`, and the chat socket answers it by
// running the turn in that mailbox's directory (`resolveEmailCwd`). The backend half carries its own
// justification; this line is recorded here so the coupling is visible from both ends rather than only
// one. If the email plugin is not installed, no workspace has this id and the branch is simply never
// taken — it fails to nothing rather than to an error.
// There WAS a third branch here — `workspace.id === 'email'` → `{ context: 'email' }` — one plugin's
// screen id hardcoded in a shared platform package. Its own comment said the general fix was a way for a
// plugin to DECLARE its chat context, and that a second hardcoded id would be the signal it was needed.
// `declared` is that fix, and the email plugin now passes it, so the branch is gone.
//
// The general fix is a way for a plugin to declare a chat context, which does not exist. Until it does,
// adding a SECOND id here is the signal that it needs to.
// It also could not fire any more, which is the part worth keeping: a plugin's workspace key is
// `screens/plugin/<name>`, so email's id became `plugin/email` the day it was extracted and the test
// silently stopped matching. Every email chat since ran with no mailbox anywhere near it. A hardcoded id
// does not fail loudly when the thing it names moves — it just stops being true.
const chatContext = declared
? declared
: workspace?.kind === 'screen' && workspace.id === 'email'
? { context: 'email' as const }
: workspace?.kind === 'dashboard'
? { context: 'dashboard' as const, contextId: workspace.key }
: {};
: workspace?.kind === 'dashboard'
? { context: 'dashboard' as const, contextId: workspace.key }
: {};
const agentPanel = useAgentPanel(panelId);