From 631443e5ae69c7603aa71abcad8a7ac73df43ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 25 Aug 2026 17:25:22 +0000 Subject: [PATCH] the chat panel has to be ours, even though the chat is not MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit naming officerdev/chat in layout.ts rendered a second Hosts panel. the shared registry is not the thing that decides: PluginScreen renders the workspace with appTypes={{ allowed, fallback: allowed[0] }} where allowed is THIS plugin's own panel keys, so an unrecognised appType silently becomes the first panel. that boundary is deliberate — its comment says a persisted layout must not be able to render another plugin's panel inside this screen — and it is a different mechanism from seedAppRegistry merging the panels, which is what the last commit's reasoning confused it with. so the panel is ours and EmbeddableChat inside it is the platform's. it is exported from officerdev for exactly this: the whole composer, transcript, queue and model picker, with no second implementation to fall behind the first. it carries a two-sentence promptPrefix, which is per-panel and costs nobody else's conversation anything. deliberately short: the skill holds the knowledge and reaches every session for two lines, so anything longer here would be the skill written twice and this would be the stale copy. all it says is where the agent is standing, which is the one thing the skill cannot know. Co-Authored-By: Claude Opus 5 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- web/ChatPanel.tsx | 38 ++++++++++++++++++++++++++++++++++++++ web/layout.ts | 14 +++++++++----- web/panels.ts | 12 +++++++++--- 3 files changed, 56 insertions(+), 8 deletions(-) create mode 100644 web/ChatPanel.tsx 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 }, ];