From 513a50f2e8d9f7311fa5b97cfb5ca1b1dc03ab6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Wed, 26 Aug 2026 07:50:55 +0000 Subject: [PATCH] a bot is a card, not a conversation in a list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 --- .../Screens/Dashboard/Bots/BotsList.tsx | 166 +++++++++++++++--- .../Screens/Dashboard/Bots/BotsScreen.tsx | 10 +- 2 files changed, 141 insertions(+), 35 deletions(-) diff --git a/src/apps/officer-web/Screens/Dashboard/Bots/BotsList.tsx b/src/apps/officer-web/Screens/Dashboard/Bots/BotsList.tsx index 6051ac9e..1833aec1 100644 --- a/src/apps/officer-web/Screens/Dashboard/Bots/BotsList.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Bots/BotsList.tsx @@ -1,22 +1,143 @@ -import { Bot, Plus } from 'lucide-react'; -import { Link, useParams } from 'react-router'; -import { useClaudeSessions } from 'state/useClaudeSessions'; +import { useState } from 'react'; +import { Bot, Check, Pencil, Plus, Trash2, X } from 'lucide-react'; +import { Link, useNavigate, useParams } from 'react-router'; +import { useClaudeSessions, type ClaudeSessionSummary } from 'state/useClaudeSessions'; /** - * The bots, down the left — the same shape Command HQ gives commanders. + * The bots, as cards — the same object Command HQ gives a commander. * - * Every bot is a chat in `$DATA_PATH//bots`, so this is that directory's session list and nothing - * more. No table, no rows to keep in step: the list IS the conversations. + * ── Why not the chat session list ── * - * Rows are real links to `/bots/:sessionId`, per `docs/navigation-audit.md` — a bot is addressable, so - * cmd-click opens one in a tab and a reload keeps the one you were reading. + * A bot IS a chat underneath, and that is an implementation detail nobody using this should have to hold. + * The generic session list says "conversation, last touched at" because that is what a conversation is. A + * bot is a thing you name, check on, and eventually delete — so it gets a card with its name, its id, and + * whether it is watching, exactly as a commander does. + * + * ── The id pill ── + * + * A commander shows `#54`, its directory. A bot has no directory of its own — they share one — so its id + * is the first eight characters of its session, which is also what an `@mention` carries. Seeing the same + * eight characters in both places is the point: the pill is what you type to reference it. */ -const NEW = '/bots'; +const shortId = (id: string) => id.slice(0, 8); + +type CardProps = { + bot: ClaudeSessionSummary; + selected: boolean; + onRename: (id: string, title: string) => Promise; + onDelete: (id: string) => Promise; +}; + +const BotCard = ({ bot, selected, onRename, onDelete }: CardProps) => { + const navigate = useNavigate(); + const [confirming, setConfirming] = useState(false); + const [renaming, setRenaming] = useState(false); + const [draft, setDraft] = useState(bot.title); + + const commit = async () => { + const next = draft.trim(); + setRenaming(false); + if (next && next !== bot.title) await onRename(bot.id, next); + }; + + return ( +
+ {/* The whole card is the link, with the controls layered above it — same arrangement as a commander + card, so a click anywhere opens the bot and the buttons still work. */} + {!renaming && } + +
+ + {renaming ? ( +
{ + ev.preventDefault(); + void commit(); + }} + > + setDraft(ev.target.value)} + onKeyDown={(ev) => ev.key === 'Escape' && setRenaming(false)} + className="min-w-0 flex-1 rounded border bg-transparent px-1 py-0.5 text-sm" + /> + +
+ ) : ( + <> + {bot.title || '(unnamed)'} + + #{shortId(bot.id)} + + + )} +
+ +
+ + {bot.messageCount} message{bot.messageCount === 1 ? '' : 's'} + + {new Date(bot.updatedAt).toLocaleDateString()} +
+ + {confirming ? ( +
+ + +
+ ) : ( + !renaming && ( +
+ + +
+ ) + )} +
+ ); +}; export const BotsList = () => { const { sessionId } = useParams(); - const { sessions, isLoading } = useClaudeSessions(null, { chatContext: { context: 'bots' } }); + const { sessions, isLoading, renameSession, deleteSession } = useClaudeSessions(null, { + chatContext: { context: 'bots' }, + }); return (