Files
proxy-manager/web/ChatPanel.tsx
T
pastilhasandClaude Opus 5 23f6d06632 the chat panel gets the header and the history it should have had
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
2026-08-25 18:30:55 +00:00

77 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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} />
);