Files
email/web/EmailChatPanel.tsx
T
pastilhasandClaude Opus 5 c538086f2b the chat says which mailbox it is in, and reads a brief instead of a prefix
the same arrangement proxy-manager got, with one difference that turned out to
matter: an email chat does not run in the plugin's chats directory, it runs in
the ACCOUNT's, beside emails.db.

three things.

the panel declares its chat context. it declared none, and it did not need to
while email was part of the platform — ChatPanelWrapper recognised the screen by
`workspace.id === 'email'` and supplied it. a plugin's workspace key is
`screens/plugin/email`, so that id became `plugin/email` on extraction day and
the branch silently stopped matching. every email chat since has run somewhere
with no mailbox anywhere near it. declaring it here is what that branch's own
comment asked for, and it carries the mailbox too, which a hardcoded screen id
never could — with two accounts the chat follows the picker.

the prompt prefix is now chat/CLAUDE.md. a prefix rides every message: paid per
turn forever, invisible as a design artefact because it lives in a typescript
constant, and visible in the one place it should not be. the file costs nothing
per turn and the person it is about can open it. it says more than the prefix
did, because it can afford to: answer from the local db and not from gmail, the
screen already shows what you would paste back, and a bulk delete over 19k rows
is not reversible from here.

the panel gets a header, so the conversation picker is scoped to this mailbox's
directory rather than absent. panelId is a prop now instead of the hardcoded
"email-chat" — two of these could never have held different conversations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
2026-08-26 18:42:59 +00:00

76 lines
3.7 KiB
TypeScript

import { useMemo } from 'react';
import { useSearchParams } from 'react-router';
import { ChatPanelWrapper, ChatHeader } from 'officerdev';
/**
* A chat, sitting under the message you are reading.
*
* ── Why the panel is ours ──
*
* `PluginScreen` renders the workspace with `appTypes={{ allowed, fallback: allowed[0] }}` where `allowed`
* is this plugin's own panel keys, so a layout naming `officerdev/chat` silently renders the fallback
* instead. That is deliberate — a persisted layout must not be able to mount another plugin's panel here.
* So the panel is ours and the chat inside it is the platform's; `ChatPanelWrapper` is exported from
* `officerdev` for exactly this, and carries the composer, transcript, queue, model picker, session
* persistence and the agent identity bar with no second implementation to fall behind the first.
*
* ── What these chats are told, and where it lives ──
*
* `chat/CLAUDE.md`, in this plugin. The platform writes it into the directory these chats run in, and
* Claude Code reads it from its own cwd at session start.
*
* There WAS a `promptPrefix` here and it is gone. A prefix rides every message: paid per turn forever,
* invisible as a design artefact because it lives in a TypeScript constant, and visible in the one place
* it should not be — three words sent, and a paragraph of preamble going out attached to them. The file
* costs nothing per turn and can be opened by the person it is about.
*/
/**
* Which mailbox this chat is about.
*
* `context: 'email'` resolves server-side to the ACCOUNT's own directory — the one holding `emails.db` —
* rather than to a chats folder. That is the whole point: the agent starts beside the database it is meant
* to answer from, and its brief can say so.
*
* This panel declared no context at all until now, and it did not need to while email was part of the
* platform: `ChatPanelWrapper` recognised the screen by `workspace.id === 'email'` and supplied it. A
* plugin's workspace key is `screens/plugin/email`, so that id became `plugin/email` and the branch
* stopped matching the day email was extracted — every email chat since has run somewhere with no mailbox
* near it. Declaring it here is what that branch's own comment asked for, and it carries the mailbox too,
* which a hardcoded screen id never could.
*
* `all` is the list's sentinel for the unified view and is NOT an address. Sending it would name a mailbox
* that does not exist, so it is dropped here and the server picks the most recently synced account.
*
* Memoised because it is passed to hooks that key on it. An object rebuilt every render is the trap that
* silently broke playback reporting in the Jellyfin player for days; it costs one line to not repeat.
*/
const useEmailChatContext = () => {
const [searchParams] = useSearchParams();
const account = searchParams.get('account');
const mailbox = account && account !== 'all' ? account : undefined;
return useMemo(() => ({ context: 'email', ...(mailbox ? { contextId: mailbox } : {}) }), [mailbox]);
};
export const EmailChatPanel = ({ panelId }: { panelId: string }) => {
const chatContext = useEmailChatContext();
return (
<div className="flex h-full min-h-0 flex-col">
<ChatPanelWrapper panelId={panelId} chatContext={chatContext} />
</div>
);
};
/**
* The panel's header: the conversation picker, scoped to this mailbox's directory.
*
* A registry `header` is mounted with `{ panelId }` only, so the context is bound here rather than passed —
* which is the whole reason this wrapper exists.
*/
export const EmailChatPanelHeader = ({ panelId }: { panelId: string }) => {
const chatContext = useEmailChatContext();
return <ChatHeader panelId={panelId} chatContext={chatContext} />;
};