import { useCallback } from 'react'; import { ChatPanelWrapper, ChatHeader } from 'officerdev'; import { useNpmChanged } from './channel'; /** * 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. * * ── What these chats are told, and where it lives ── * * `chat/CLAUDE.md`, in this plugin. The platform copies it into the chats directory whenever a chat starts * there, 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 — the owner sent three words and watched a paragraph of preamble go out attached to * them. The file costs nothing per turn and can be opened by the person it is about. */ 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 }) => { const [, npmChanged] = useNpmChanged(); /** * A turn that ran tools may have changed NPM, so every list on this screen re-reads. * * This is the whole reason the panel is worth having beside the lists rather than in `/chat`. An agent * deleted two proxy hosts on 2026-08-25 and both stayed on screen afterwards, because nothing told the * panels — the person had the answer in the transcript and a stale list six inches above it. * * Gated on `hadToolCalls`, which is the only discrimination that matters: a turn that only talked * changed nothing, and re-reading after every sentence would put six requests behind every reply. */ const onTurnComplete = useCallback( (hadToolCalls: boolean) => { if (hadToolCalls) npmChanged(); }, [npmChanged], ); return (