it looked like the generic chat panel because it WAS one, and that leaks the implementation at exactly the wrong moment: a bot is a chat underneath, and nobody using this should have to hold that. the session list says "conversation, last touched" because that is what a conversation is. so it gets Command HQ's object instead — a card with a name, an id pill, rename and delete, selected ring, controls on hover. same arrangement: the whole card is the link with the buttons layered over it, so a click anywhere opens it and the controls still work. the pill is the first eight characters of the session, which is also what an @mention carries. that is the point rather than a coincidence — the thing shown on the card is the thing typed to reference it. the panel's own header picker is gone. the cards are the list, and a second picker in the panel bar is the same choice offered twice. command hq does not have one either. deleting the selected bot navigates back to /bots, or the route points at something that no longer exists and renders an empty chat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
60 lines
2.6 KiB
TypeScript
60 lines
2.6 KiB
TypeScript
import type { LayoutNode } from 'officerdev';
|
|
import { WorkspaceView, ChatPanelWrapper } from 'officerdev';
|
|
import { useParams } from 'react-router';
|
|
import { useAuthoredLayout } from 'state/useAuthoredLayout';
|
|
import { defaultLayout } from './defaultLayout';
|
|
import { BotsList } from './BotsList';
|
|
|
|
/**
|
|
* `/bots` — one sentence arms a watch, and the conversation stays open to widen it.
|
|
*
|
|
* ── The same shape as Command HQ ──
|
|
*
|
|
* A narrow list on the left, the conversation on the right, and the selection in the URL as a route pair:
|
|
* `/bots` is the new-bot state, `/bots/:sessionId` is one of them. That is the convention
|
|
* `docs/navigation-audit.md` sets and the one Command HQ already follows, so a bot is linkable and a
|
|
* reload keeps the one you were reading.
|
|
*
|
|
* ── Why the panels are supplied here rather than registered ──
|
|
*
|
|
* Every bot runs in one shared directory, which the server resolves from `context: 'bots'`. The registry's
|
|
* `officerdev/chat` derives its context from the workspace and would land on none of it — it would list
|
|
* and resume the caller's HOME, which is exactly what this screen did on its first outing. `components` is
|
|
* the prop for a screen that supplies its own chat panel, and it carries the header, so the picker lists
|
|
* bots rather than every chat on the machine.
|
|
*
|
|
* The instructions a bot runs under are `$DATA_PATH/<email>/bots/CLAUDE.md`, written by the server on
|
|
* every turn — `api/bots/brief.ts`.
|
|
*/
|
|
|
|
const CHAT_CONTEXT = { context: 'bots' } as const;
|
|
|
|
const BotsChat = ({ panelId }: { panelId: string }) => {
|
|
const { sessionId } = useParams();
|
|
return (
|
|
<div className="flex h-full min-h-0 flex-col">
|
|
{/* `sessionId ?? null` — null is "the screen chose a new chat", which is not the same as having no
|
|
opinion. On `/bots` with no id that is exactly right: an empty composer waiting for the sentence. */}
|
|
<ChatPanelWrapper panelId={panelId} chatContext={CHAT_CONTEXT} sessionId={sessionId ?? null} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const BotsScreen = () => {
|
|
const workspace = useAuthoredLayout('screens/bots', defaultLayout as LayoutNode);
|
|
|
|
return (
|
|
<WorkspaceView
|
|
workspace={workspace}
|
|
locked
|
|
appTypes={{ allowed: ['bots-list', 'bots-chat'], fallback: 'bots-chat' }}
|
|
components={{
|
|
'bots-list': BotsList,
|
|
// No header picker: the cards on the left are the list, and a second one in the panel bar would be
|
|
// the same choice offered twice. Command HQ does not have one either, for the same reason.
|
|
'bots-chat': BotsChat,
|
|
}}
|
|
/>
|
|
);
|
|
};
|