Files
email/web/EmailChatPanel.tsx
T
pastilhas 6efc07c09f show a watch-with-commander button on the email chat header
ChatHeader already supports commanderLabel; this panel just never passed one.
Version bumped to match this release's tag.
2026-09-14 21:01:17 +01:00

76 lines
3.8 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} commanderLabel="Email" />;
};