a commander starts its own conversation, instead of being handed one
Starting a commander meant writing a good opening prompt, which is the hard part of the whole thing — so in practice somebody else wrote it and pasted it in. That is not a flow, it is a person acting as the intake form. Now the two decisions that cannot be guessed stay with the owner (a name and a working directory) and the rest is a button. It sends a brief the MODEL reads — "I have not told you what I want yet, orient yourself and ask me one question" — while the transcript shows "Set me up.", which is what a person would have typed. That split is `effectivePrompt` vs `displayText` and the chat path already had it. What to ask, in what order, and to read the directory BEFORE asking anything, all live in the Commanders skill under "When you started yourself". Deliberately not here: capabilities are re-read from disk every run, so rewording a question costs nothing, while the same words in commander-block.ts cost a restart each time. The block carries a pointer and no doctrine. Orienting first is what makes this work on a real request. Told "automate the platform deploys", a commander that has read the directory finds `deploy/`, the previous releases and the deploy skill, and asks whether that is still how you want it — instead of inventing a scheme next to the one already there. `sendPrompt` gained an options object rather than a tenth positional parameter. Nine is already past where a call site reads as a row of undefineds. The composer stays enabled beside the button: typing your own opener still works, it just is not the only way in any more. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router';
|
||||
import { Check, FolderOpen, Radar, Trash2, Unplug, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from '@/components/ui/sonner';
|
||||
import { EditableTitle } from '@/components/EditableTitle';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
@@ -65,6 +66,47 @@ const NewCommanderGuide = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* The whole opening move, as one button.
|
||||
*
|
||||
* Writing a good first prompt is the hard part of starting a commander, and asking somebody to do it before
|
||||
* they have talked to it gets it done badly or not at all. So this sends the opener instead: a brief the
|
||||
* MODEL reads telling it to run the interview, while the transcript shows the short line a person would
|
||||
* have written. That split is `effectivePrompt` vs `displayText`, which the chat path already had.
|
||||
*
|
||||
* The instruction itself is deliberately thin. What to ask, in what order, and to orient in the directory
|
||||
* before asking anything at all, all live in the Commanders skill — which is re-read from disk every run, so
|
||||
* it can be reworded without a deploy. Duplicating any of it here would create a second copy to drift.
|
||||
*/
|
||||
const START_BRIEF = [
|
||||
'I have given you a name and a working directory. I have not told you what I want yet — that is what this',
|
||||
'conversation is for, and I would rather answer your questions than write a specification up front.',
|
||||
'',
|
||||
'Orient yourself here first, then ask me one question.',
|
||||
].join('\n');
|
||||
|
||||
const StartCommanderButton = ({ ready, blockedBy, onStart }: StartCommanderButtonProps) => (
|
||||
<div className="shrink-0 border-b border-border px-5 py-4">
|
||||
<div className="mx-auto flex max-w-2xl flex-col items-start gap-2">
|
||||
<Button onClick={onStart} disabled={!ready} size="lg">
|
||||
Start the conversation
|
||||
</Button>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{ready
|
||||
? 'It will look around this directory, then ask you what to watch for.'
|
||||
: `${blockedBy} — then this starts it.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
type StartCommanderButtonProps = {
|
||||
ready: boolean;
|
||||
/** What is still missing, phrased to finish the sentence in the hint below the button. */
|
||||
blockedBy: string;
|
||||
onStart: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The two decisions a commander cannot be started without.
|
||||
*
|
||||
@@ -236,6 +278,24 @@ function NewCommander({
|
||||
{chat.messages.length === 0 && (
|
||||
<>
|
||||
<CommanderSetup name={name} onName={setName} cwd={cwd} onPickDirectory={() => setPickerOpen(true)} />
|
||||
<StartCommanderButton
|
||||
ready={ready}
|
||||
blockedBy={!name.trim() ? 'Name it first' : 'Choose a working directory'}
|
||||
onStart={() =>
|
||||
// Positional, and the shape is why `opts` was added rather than a tenth slot: displayText is #7.
|
||||
chat.sendPrompt(
|
||||
START_BRIEF,
|
||||
undefined,
|
||||
undefined,
|
||||
cwd ? { path: cwd } : undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'Set me up.',
|
||||
undefined,
|
||||
{ selfStarted: true },
|
||||
)
|
||||
}
|
||||
/>
|
||||
<NewCommanderGuide />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -792,6 +792,20 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
* exact message that starts the watching turn, not a later one.
|
||||
*/
|
||||
armWatcher?: boolean,
|
||||
/**
|
||||
* Anything else this turn needs to say about ITSELF rather than about its content.
|
||||
*
|
||||
* An object rather than a tenth positional: nine is already past the point where a call site reads as
|
||||
* a row of undefineds, and every flag added here would make that worse for every existing caller.
|
||||
*/
|
||||
opts?: {
|
||||
/**
|
||||
* The owner pressed Start instead of typing an opening message, so this turn is a request for the
|
||||
* commander to interview them — see `commanderBlock`. Only the client that rendered the button can
|
||||
* know this, which is why it rides the message rather than being derived on the server.
|
||||
*/
|
||||
selfStarted?: boolean;
|
||||
},
|
||||
) {
|
||||
// Mark session as started on first message
|
||||
if (!hasStarted) {
|
||||
@@ -832,6 +846,7 @@ export function useChat(initialSessionId?: string, initialModel?: string | null,
|
||||
type: 'chat',
|
||||
prompt: text,
|
||||
...(displayText ? { displayText } : {}),
|
||||
...(opts?.selfStarted ? { selfStarted: true } : {}),
|
||||
sessionId: sessionIdRef.current,
|
||||
...(selectedModel ? { model: selectedModel } : {}),
|
||||
...(cwdParam?.path ? { cwd: cwdParam.path } : {}),
|
||||
|
||||
Reference in New Issue
Block a user