a new commander pane explains itself, in skeleton

Starting a commander currently means knowing to open the background-job dialog and arm a watcher from it.
That is not discoverable, and it reads as configuration when the real answer is "talk to it" — so the dialog
stops being the way in, and the pane explains the way in instead.

EmbeddableChat's own empty state is "Send a message to start". True, and it tells a first-time user nothing
about what to send.

The copy is NOT written, by agreement: the instructions have to describe a flow that does not exist yet, and
writing them first produces text that is wrong in a way nobody notices. What is here is the structure, plus
the parts already true whatever the flow becomes — what a commander is, and that it begins as an ordinary
conversation.

Sections carry a `provisional` flag that renders a visible "draft" marker. That is the load-bearing detail:
this ships on every deploy whether the copy is ready or not, so a half-written guide must not be able to
look finished. Clearing the flag is the last step of writing a section, not a formatting choice.

Data rather than JSX, so filling it in later is editing one array instead of hunting through markup — the
same shape as the shortcuts registry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 16:30:11 +00:00
co-authored by Claude Opus 5
parent 228f2f4638
commit 29d998b6c3
2 changed files with 85 additions and 0 deletions
@@ -11,6 +11,7 @@ import type { ChatMessage } from '../Chat/types';
import { categoryFromParam, commandHqListPath, commandHqSessionPath } from './command-hq-routes'; import { categoryFromParam, commandHqListPath, commandHqSessionPath } from './command-hq-routes';
import { useCommandHqCategories, useCommandHqCommander, useCommandHqCommanderActions } from './useCommandHq'; import { useCommandHqCategories, useCommandHqCommander, useCommandHqCommanderActions } from './useCommandHq';
import { formatCommanderCost, formatTokens } from './format'; import { formatCommanderCost, formatTokens } from './format';
import { NEW_COMMANDER_GUIDE } from './newCommanderGuide';
// Right panel of the /command-hq workspace. Three states, chosen by the URL alone (no channel — panels // Right panel of the /command-hq workspace. Three states, chosen by the URL alone (no channel — panels
// read the route directly, per `docs/navigation-audit.md`): // read the route directly, per `docs/navigation-audit.md`):
@@ -32,6 +33,37 @@ function EmptyState() {
); );
} }
/**
* The instructions shown in an empty new-commander pane.
*
* Deliberately plain and deliberately unfinished — see `newCommanderGuide.ts`. A `provisional` section
* carries a visible marker so a guide that is still being written cannot be mistaken for one that is done,
* which matters because this ships on every deploy whether the copy is ready or not.
*/
const NewCommanderGuide = () => (
<div className="shrink-0 overflow-y-auto border-b border-border px-5 py-4">
<div className="mx-auto flex max-w-2xl flex-col gap-4">
{NEW_COMMANDER_GUIDE.map((section) => (
<section key={section.title}>
<h3 className="flex items-center gap-2 text-sm font-semibold text-foreground">
{section.title}
{section.provisional && (
<span className="rounded bg-warning/15 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-warning">
draft
</span>
)}
</h3>
{section.body.map((paragraph) => (
<p key={paragraph} className="mt-1 text-sm leading-relaxed text-muted-foreground">
{paragraph}
</p>
))}
</section>
))}
</div>
</div>
);
/** /**
* A commander started in THIS pane, and it keeps the same live chat for as long as the pane is open. * A commander started in THIS pane, and it keeps the same live chat for as long as the pane is open.
* *
@@ -103,6 +135,9 @@ function NewCommander({
`useEmbeddableChat(params)` builds the manager from these props, and `manager.context` is what the `useEmbeddableChat(params)` builds the manager from these props, and `manager.context` is what the
composer reads to decide whether to offer "exit and hand back, then re-arm". Passing it to the hook composer reads to decide whether to offer "exit and hand back, then re-arm". Passing it to the hook
and not to the component left that radio hidden inside a commander — the one place it exists for. */} and not to the component left that radio hidden inside a commander — the one place it exists for. */}
{/* Before the first message, the pane explains itself. `EmbeddableChat`'s own empty state is "Send a
message to start", which is true and tells a first-time user nothing about what to send. */}
{chat.messages.length === 0 && <NewCommanderGuide />}
<EmbeddableChat chat={chat} sessionId={undefined} context="command-hq" className="min-h-0 flex-1" /> <EmbeddableChat chat={chat} sessionId={undefined} context="command-hq" className="min-h-0 flex-1" />
</div> </div>
); );
@@ -0,0 +1,50 @@
/*
What the pane says before a new commander has been spoken to.
── Why this is data, and why it is a skeleton ──
The creation flow is being rebuilt. Starting a commander currently means knowing to open the background-job
dialog and arm a watcher from it, which is not discoverable and reads as configuration when it is really
just "talk to it" — so the dialog stops being the way in, and this pane explains the way in instead.
The copy is NOT written yet, by agreement: the instructions have to describe a flow that does not exist
yet, and writing them before the flow is decided produces text that is wrong in a way nobody notices.
What is here is the structure plus the parts that are already TRUE regardless of what the flow becomes —
what a commander is, and that it begins as an ordinary conversation.
Sections marked `provisional` render with a visible marker, so a half-written guide cannot ship looking
finished. Removing the flag is the last step of writing one, not a formatting choice.
*/
export type GuideSection = {
title: string;
/** One paragraph per entry. Kept as an array so a section can grow without touching the renderer. */
body: string[];
/** True until the copy is settled. Renders a marker; the whole point is that it is visible. */
provisional?: boolean;
};
export const NEW_COMMANDER_GUIDE: GuideSection[] = [
{
title: 'What a commander is',
body: [
'An agent that keeps working when you are not here — it waits for something to happen, acts on it, and reports back.',
'It is an ordinary conversation. You start by telling it what to watch for and what to do about it, in your own words.',
],
},
{
title: 'Starting one',
provisional: true,
body: [
'Describe the job here, as you would to anybody. Say what it should watch, where, and what to do when it fires.',
'TODO: the concrete steps, once the creation flow is settled. This replaces "open the background-job dialog and arm a watcher", which nobody could be expected to guess.',
],
},
{
title: 'While it runs',
provisional: true,
body: [
'TODO: what the owner sees — where its cost shows, how to read what its spawned agents are doing, and how to stop it.',
],
},
];