command hq: delete a commander, from the grid card and the detail bar

DELETE /command-hq/commanders/:sessionId — deletes the transcript itself via
deleteClaudeSession (the same function DELETE /chat/sessions/:id calls, so a
/clear chain goes with it, every part), then the command_hq_sessions row via
deleteCommandHqSession (existing, exported, never previously called from a
route — its docstring said as much: "no product path to this yet").

Deliberately real deletion, not an unfile-back-to-/chat path — there still
isn't one of those, and this doesn't create one. Documented next to "The
/chat <-> /command-hq relationship" so it doesn't read as contradicting the
"a session never moves the other direction" rule directly above it.

Frontend: both the grid card and the detail bar's meta row get an inline
confirm (trash icon -> check/cancel), same shape as ChatHistory's own
SessionRow delete. The grid card's delete control is a sibling of the card's
<Link>, not nested inside it, per docs/navigation-audit.md — the card is a
stretched full-cover Link (absolute inset-0, default stacking) with the
delete control in its own stacking context (z-10) on top of it, so the card
stays click-anywhere except where the trash icon sits. Deleting the commander
currently open in the detail panel navigates back to the bare list rather
than leaving it pointed at a 404.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 10:46:31 +00:00
co-authored by Claude Opus 5
parent a335fad9a6
commit f77a41d26a
5 changed files with 228 additions and 64 deletions
+30
View File
@@ -10,6 +10,7 @@ import {
listClaudeSessions,
loadClaudeSessionById,
renameClaudeSession,
deleteClaudeSession,
type ChatIdentity,
type ClaudeSessionSummary,
} from '../chat/claude-sessions';
@@ -24,6 +25,7 @@ import {
getCommandHqSession,
setCommandHqSessionCategory,
purgeCommandHqSessions,
deleteCommandHqSession,
disarmCommandHqWatcher,
getCommandHqCostSummary,
listCommandHqTurns,
@@ -446,6 +448,34 @@ commandHqRouter.patch('/commanders/:sessionId', async (ctx) => {
return ctx.json({ ok: true });
});
/**
* DELETE /command-hq/commanders/:sessionId
*
* Deletes the commander outright: the transcript itself (via `deleteClaudeSession`, the same function
* `DELETE /chat/sessions/:id` calls — a `/clear` chain goes with it, every part), then the
* `command_hq_sessions` row. Unlike everywhere else in this file, this is NOT the "row disagrees with
* disk, so hide it" pattern `purgeCommandHqSessions` exists for — it is the one place that makes the two
* agree by deleting the thing itself, on purpose, because the owner asked to.
*
* A watcher still armed at the time of deletion is left as-is rather than disarmed first: the transcript
* is gone either way, `armCommandHqWatcher`/`recordCommandHqWatcherFired` can no longer find a row to
* update once this returns, and the running background process (if any) dies on its own the same way it
* would if the harness process were killed out from under any other conversation — not a new failure
* mode this route has to account for.
*/
commandHqRouter.delete('/commanders/:sessionId', async (ctx) => {
const user = ctx.get('user');
const who = await commandHqIdentity(user);
const sessionId = ctx.req.param('sessionId');
const row = await getCommandHqSession(user.id, sessionId);
if (!row) throw errors.NOT_FOUND('not a commander');
deleteClaudeSession(who, getCommandersDir(who.email), sessionId);
await deleteCommandHqSession(user.id, sessionId);
return ctx.json({ ok: true });
});
/**
* POST /command-hq/purge
*