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 (
); }; /** * 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 ; };