a commander is a commander wherever it is opened
Fixing a regression I introduced last night. Commander rows in the Live list were changed to open in /chat rather than navigating to Command HQ — right, and it cost the composer its commander controls, because `isCommander` was computed from the ROUTE: `manager.context === 'command-hq'`. So the same commander, rendered on a different screen, silently lost "exit and hand back, then re-arm" — the only fire behaviour a watcher wants, and the one the dialog makes default inside a commander. GET /chat/sessions/:id now reports isCommander, derived from the session's own cwd by the same isCommandersDir already used by the sessions list and /chat/live. The answer travels with the conversation. The composer reads one value, manager.isCommander, true when EITHER source says so — and both are needed. The screen knows when Command HQ starts a commander, before a session or a transcript exists for any route to report on. The session knows once it does, wherever it is opened. Screen-only was this bug; session-only would have broken the brand-new composer, which is where the whole thing starts. Verified on a real commander rather than by reading: 172cdd8c resolves cwd=…/commanders and isCommander=true. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,8 @@ type ResumeState = {
|
||||
messages: ChatMessage[];
|
||||
total: number;
|
||||
offset: number;
|
||||
/** Carried so the composer offers a commander's own controls here, not only in Command HQ. */
|
||||
isCommander: boolean;
|
||||
};
|
||||
|
||||
// Transcripts get extremely long, so only the tail is loaded up front; the chat pages older messages in
|
||||
@@ -104,6 +106,9 @@ const ChatPanelInner = ({
|
||||
promptPrefix={promptPrefix}
|
||||
panelId={panelId}
|
||||
sessionId={sessionKey ?? undefined}
|
||||
// From the SESSION, so a commander opened here gets a commander's composer. `chatContext` may also
|
||||
// set it via `context`; either being true is enough — see `manager.isCommander`.
|
||||
isCommander={resume?.isCommander === true}
|
||||
{...chatContext}
|
||||
/>
|
||||
</div>
|
||||
@@ -221,6 +226,7 @@ export const ChatPanelWrapper = ({ panelId, promptPrefix }: ChatPanelWrapperProp
|
||||
messages: detail.messages as unknown as ChatMessage[],
|
||||
total: detail.total,
|
||||
offset: detail.offset,
|
||||
isCommander: detail.isCommander === true,
|
||||
});
|
||||
} catch {
|
||||
// A stale id — the transcript was deleted or pruned from under us. Open a NEW conversation rather
|
||||
|
||||
@@ -23,6 +23,14 @@ type EmbeddableChatProps = {
|
||||
chat?: UseChatType;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
/**
|
||||
* This SESSION is a commander, as reported by `GET /chat/sessions/:id`.
|
||||
*
|
||||
* Separate from `context`, which says which SCREEN is rendering. Both can make the composer offer the
|
||||
* handoff watcher and only this one is true of the conversation wherever it is opened — which is the bug:
|
||||
* a commander opened in /chat had `context: 'chat'` and silently lost the control.
|
||||
*/
|
||||
isCommander?: boolean;
|
||||
onMessageComplete?: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ type UseEmbeddableChatParams = {
|
||||
chat?: UseChatType;
|
||||
context?: string;
|
||||
contextId?: string;
|
||||
/** This session is a commander, per `GET /chat/sessions/:id`. See the note where it is surfaced. */
|
||||
isCommander?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -611,6 +613,15 @@ export function useEmbeddableChat(params: UseEmbeddableChatParams, onMessageComp
|
||||
// place a `handoff` watcher can be promised — see BackgroundJobDialog — and it was invisible to the
|
||||
// composer, so the option was never rendered anywhere at all.
|
||||
context: params.context,
|
||||
/*
|
||||
One answer to "is this a commander", from either of the two things that can know.
|
||||
|
||||
The SCREEN knows when Command HQ starts one, before the session exists and before any route could
|
||||
report on it. The SESSION knows once it has a transcript, wherever it is opened. Neither alone is
|
||||
enough: screen-only was the original bug (a commander in /chat lost its controls) and session-only
|
||||
would lose the brand-new composer, which has no session yet.
|
||||
*/
|
||||
isCommander: params.context === 'command-hq' || params.isCommander === true,
|
||||
messages,
|
||||
historicalCount,
|
||||
streamingText,
|
||||
|
||||
@@ -199,12 +199,16 @@ export const InputArea = ({ manager }: InputAreaProps) => {
|
||||
`handoff` — exit, act, re-arm — is only offered inside a Command HQ commander, because it is a
|
||||
promise the platform has to keep rather than only a sentence in the prompt.
|
||||
|
||||
Read from the MANAGER rather than recomputed from the route, which is what it was: a commander
|
||||
opened from /chat reported `context: 'chat'` and lost the option, having gained it two hours
|
||||
earlier. `manager.isCommander` is true when either the screen or the session says so.
|
||||
|
||||
This prop was never passed by anybody, so `isCommander` defaulted to false everywhere and the
|
||||
option was rendered NOWHERE — including in a commander, where the dialog's own code makes it the
|
||||
default. The whole mechanism existed: the fire behaviour, the contract, `armWatcher` reaching the
|
||||
server. Only the radio button was missing, so the feature looked unbuilt.
|
||||
*/
|
||||
isCommander={manager.context === 'command-hq'}
|
||||
isCommander={manager.isCommander}
|
||||
onSubmit={(intent, preamble, armWatcher) => {
|
||||
// Claimed by the next job to start, which is how the preview tells this one apart from the
|
||||
// background jobs an agent starts on its own.
|
||||
|
||||
@@ -96,6 +96,14 @@ export type ClaudeSessionDetail = {
|
||||
title?: string | null;
|
||||
/** Transcripts spliced into `messages` — `> 1` when this is a merged `/clear` chain. */
|
||||
partCount?: number;
|
||||
/**
|
||||
* A commander rather than an ordinary conversation, derived server-side from this session's own cwd.
|
||||
*
|
||||
* It travels with the SESSION and not with the screen, which is the whole point. The composer decides
|
||||
* whether to offer "exit and hand back, then re-arm" — the only fire behaviour a watcher wants — and used
|
||||
* to decide it from the route, so the same commander opened in /chat quietly lost it.
|
||||
*/
|
||||
isCommander?: boolean;
|
||||
};
|
||||
|
||||
export type ClaudePwd = { cwd: string; sessionCount: number; updatedAt: string; isDefault: boolean };
|
||||
|
||||
Reference in New Issue
Block a user