diff --git a/web/ChatPanel.tsx b/web/ChatPanel.tsx new file mode 100644 index 0000000..f5cf61c --- /dev/null +++ b/web/ChatPanel.tsx @@ -0,0 +1,38 @@ +import { EmbeddableChat } 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(' '); + +export const ChatPanel = ({ panelId }: { panelId: string }) => ( + +); diff --git a/web/layout.ts b/web/layout.ts index a7c057b..b46c14b 100644 --- a/web/layout.ts +++ b/web/layout.ts @@ -2,11 +2,15 @@ import type { LayoutNode } from 'officerdev'; // Hosts wide, certificates beside it, a chat across the bottom. // -// ── Why the chat panel is the platform's and not one of ours ── +// ── The chat panel is ours; the chat inside it is the platform's ── // -// `officerdev/chat` is a panel app in the shared registry — `seedAppRegistry` merges the platform's panels -// and every plugin's into ONE registry, so naming it here is all it takes. Shipping a second, simpler chat -// from this plugin would mean maintaining a second chat, and the first one to fall behind would be ours. +// Naming `officerdev/chat` here does NOT work, and the way it fails is quiet: `PluginScreen` renders the +// workspace with `appTypes={{ allowed, fallback: allowed[0] }}` where `allowed` is this plugin's own panel +// keys, so an unrecognised appType becomes the FIRST panel — which is how this layout briefly rendered two +// Hosts panels. The shared registry and the layout allow-list are different things. +// +// So `proxy-manager-chat` is our panel and `EmbeddableChat` inside it is the platform's component. No +// second chat to maintain, and nothing for the allow-list to reject. // // It gets a third of the height rather than a column, because a conversation reads badly in a narrow // column and the two lists do not: they are scannable at any width, and a chat is not. @@ -34,6 +38,6 @@ export const defaultLayout: LayoutNode = { }, size: 66, }, - { node: { type: 'panel', id: 'proxy-manager-chat', appType: 'officerdev/chat' }, size: 34 }, + { node: { type: 'panel', id: 'proxy-manager-chat', appType: 'proxy-manager-chat' }, size: 34 }, ], }; diff --git a/web/panels.ts b/web/panels.ts index 685823a..c2a4ba9 100644 --- a/web/panels.ts +++ b/web/panels.ts @@ -1,10 +1,15 @@ -import { Route, ShieldCheck } from 'lucide-react'; +import { MessageSquare, Route, ShieldCheck } from 'lucide-react'; import type { AppRegistryMeta } from 'officerdev'; import { HostsPanel } from './HostsPanel'; import { CertificatesPanel } from './CertificatesPanel'; +import { ChatPanel } from './ChatPanel'; -// Two panels. Hosts is what the plugin is for; certificates is the thing that quietly breaks and is -// worth seeing beside it rather than a click away. +// Three panels. Hosts is what the plugin is for; certificates is the thing that quietly breaks and is +// worth seeing beside it rather than a click away; chat is where you ask about either of them. +// +// The chat panel has to be OURS even though the chat inside it is the platform's — `PluginScreen` pins the +// layout to `allowed = panels.map(p => p.key)`, so naming `officerdev/chat` in layout.ts renders the +// fallback instead. See ChatPanel.tsx. // // `availableOnPanel: false` — these belong to this plugin's screen, not to the generic panel picker. export const appRegistryMetas: AppRegistryMeta[] = [ @@ -16,4 +21,5 @@ export const appRegistryMetas: AppRegistryMeta[] = [ component: CertificatesPanel, availableOnPanel: false, }, + { key: 'proxy-manager-chat', name: 'Chat', icon: MessageSquare, component: ChatPanel, availableOnPanel: false }, ];