ChatPanelWrapper, not EmbeddableChat — one level up, and the session persistence, the agent identity bar, the spawned-by bar and resume-the-newest all come with it. mounting the composer directly meant reimplementing those or going without. and a header: the conversation picker, scoped to this plugin's own directory, with new chat, inline rename and delete. none of it is new — it is what a dashboard's chat panel has always had, pointed at a context instead of a path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
77 lines
3.9 KiB
TypeScript
77 lines
3.9 KiB
TypeScript
import { ChatPanelWrapper, ChatHeader } from 'officerdev';
|
||
|
||
/**
|
||
* A chat, sitting under the two lists.
|
||
*
|
||
* ── Why this exists instead of naming `officerdev/chat` in the layout ──
|
||
*
|
||
* It was tried, and it produced two Hosts panels. `PluginScreen` renders the workspace with
|
||
* `appTypes={{ allowed, fallback: allowed[0] }}` where `allowed` is **this plugin's own panel keys**, so a
|
||
* layout naming anything else silently falls back to the first one. That is deliberate and its comment
|
||
* says why: a persisted layout must not be able to render another plugin's panel inside this screen.
|
||
*
|
||
* The registry being shared is a separate thing from the layout being allowed to name what is in it. Both
|
||
* are true; only the second one decides this.
|
||
*
|
||
* So the panel is ours and the chat inside it is the platform's. `EmbeddableChat` is exported from
|
||
* `officerdev` for precisely this — the whole composer, transcript, queue and model picker, with no second
|
||
* implementation to fall behind the first.
|
||
*
|
||
* ── The prefix ──
|
||
*
|
||
* `promptPrefix` is described in `ChatPanelWrapper` as "a system prompt only this app understands". It is
|
||
* per-panel rather than per-session-wide, so it costs nothing to anybody else's conversation.
|
||
*
|
||
* Kept to two sentences on purpose. The Proxy Manager SKILL carries the real knowledge and reaches every
|
||
* conversation on the machine at a cost of two lines; this only says where the agent is standing, which is
|
||
* the one thing the skill cannot know. Anything longer would be the skill written twice, and the copy here
|
||
* would be the stale one.
|
||
*/
|
||
const PREFIX = [
|
||
'This chat is a panel on the Proxy Manager screen, beside a list of this machine’s proxy hosts and a list',
|
||
'of its TLS certificates. Questions about domains, forwarding, whether a site is up, or when a certificate',
|
||
'expires are about THIS machine — read the Proxy Manager skill before answering one.',
|
||
].join(' ');
|
||
|
||
/**
|
||
* The wrapper is not decoration. `EmbeddableChat` is handed `flex-1 min-h-0`, which needs a flex COLUMN
|
||
* with a real height above it — without one the composer floats mid-panel and the transcript overflows
|
||
* below the fold, which is exactly what shipping it bare produced. `ChatPanelWrapper` has the same div for
|
||
* the same reason.
|
||
*/
|
||
/**
|
||
* The context this plugin's chats belong to.
|
||
*
|
||
* The platform turns it into `$DATA_PATH/<email>/plugins/proxy-manager/chats` — for the turn, and for the
|
||
* header's session list — and the path is never sent from here. `docs/commander-directory.md` has why a
|
||
* client does not choose a cwd; `cwdOf` in `api/chat/chat.ts` has the listing half.
|
||
*
|
||
* What the directory buys beyond tidiness: Claude Code reads a `CLAUDE.md` from its cwd at session start,
|
||
* so anything these chats should know can live there instead of in every conversation on the machine.
|
||
*/
|
||
const CHAT_CONTEXT = { context: 'plugin', contextId: 'proxy-manager' } as const;
|
||
|
||
/**
|
||
* `ChatPanelWrapper`, not `EmbeddableChat` — one level up, and everything below it comes free.
|
||
*
|
||
* The wrapper already carries the session persistence (`usePanelSession`, so this panel stays on its own
|
||
* conversation across a reload), the agent identity bar, the spawned-by bar, and the resume-the-newest
|
||
* behaviour. Mounting the composer directly meant reimplementing all of that or going without it, which is
|
||
* what this panel was doing.
|
||
*/
|
||
export const ChatPanel = ({ panelId }: { panelId: string }) => (
|
||
<div className="flex h-full min-h-0 flex-col">
|
||
<ChatPanelWrapper panelId={panelId} promptPrefix={PREFIX} chatContext={CHAT_CONTEXT} />
|
||
</div>
|
||
);
|
||
|
||
/**
|
||
* The panel's header: the conversation picker, scoped to this plugin'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 ChatPanelHeader = ({ panelId }: { panelId: string }) => (
|
||
<ChatHeader panelId={panelId} chatContext={CHAT_CONTEXT} />
|
||
);
|