From 1c47d0bba7086b0569f460052e626c91c2bc3e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 22 Aug 2026 19:11:12 +0000 Subject: [PATCH] a commander is named and given a directory before it can be spoken to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The third of the three, and the one the owner will actually see. The new-commander card now collects both decisions above the guide, and the composer refuses input until they are made — with the reason in the placeholder rather than a validation message after the fact. Both are chosen first because neither is cheap afterwards. The directory decides where every relative path in every turn resolves and which project group holds the transcripts, so changing it later strands a conversation away from its own history. The name has to reach the transcript the moment it exists, because Claude Code names a new session from its first prompt and the platform's rename only wins by getting there first. The directory is a BUTTON, not a text field: it is an absolute server path, and typing one is how a commander ends up running somewhere that does not exist. `DirPickerModal` already existed for /chat and browses the account's own tree. CAUGHT BEFORE SHIPPING, and only by checking: I first passed `cwd` and `commanderName` as `useChat` options and `disabled` as an EmbeddableChat prop. None of the three existed. It typechecked anyway, because object SPREADS bypass excess-property checking — `...(cwd ? { cwd } : {})` is invisible to it. That is the fourth time today a silently-ignored prop has cost something, so the props are real now: - `commanderName` is a `useChat` option on the send payload, and rides EVERY turn rather than only the first. The server applies it once; sending it again costs bytes. Sending it once would lose it if that turn failed and was retried — and a commander named after its second prompt is exactly what this prevents. - `disabled` is a real EmbeddableChat prop carrying a reason, threaded to the textarea's NATIVE disabled — which also removes it from the tab order and blocks a paste, neither of which a guard on send would do. - the directory goes to EmbeddableChat's existing `cwd` prop, because that is what feeds `sendPrompt`'s `cwdParam`. Two values chosen in one place travelling by two paths, noted where somebody would otherwise have to discover it. Co-Authored-By: Claude Opus 5 --- .../Chat/EmbeddableChat/EmbeddableChat.tsx | 10 +- .../src/apps/Chat/components/InputArea.tsx | 8 +- .../src/apps/CommandHQ/CommandHqDetail.tsx | 115 +++++++++++++++++- .../officerdev/src/hooks/useChat.ts | 11 ++ 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx index 3207af75..250d8977 100644 --- a/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/EmbeddableChat/EmbeddableChat.tsx @@ -31,6 +31,14 @@ type EmbeddableChatProps = { * a commander opened in /chat had `context: 'chat'` and silently lost the control. */ isCommander?: boolean; + /** + * Refuse input entirely, with a reason the composer shows. + * + * For a surface where something must be chosen before a conversation can start — a new commander needs a + * name and a working directory, both unchangeable afterwards. Disabling the composer is the only honest + * place to enforce that: anywhere later and the person has already written what they meant to say. + */ + disabled?: false | { reason: string }; onMessageComplete?: () => void; }; @@ -43,7 +51,7 @@ export const EmbeddableChat = ({ className, onMessageComplete, ...params }: Embe return (
- +
); }; diff --git a/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx b/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx index fdffa53b..ee6a3f60 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/InputArea.tsx @@ -16,9 +16,11 @@ import { BackgroundTaskTray } from './BackgroundTaskTray'; type InputAreaProps = { manager: UseEmbeddableChatType; + /** When set, input is refused and the reason is shown in place of the composer's own hints. */ + disabled?: false | { reason: string }; }; -export const InputArea = ({ manager }: InputAreaProps) => { +export const InputArea = ({ manager, disabled }: InputAreaProps) => { const [loginOpen, setLoginOpen] = useState(false); // Only for a commander, and only once it has an id. Everything a commander's loop needs in order to be // stoppable and deletable hangs off these two paths — see api/command-hq/spawned.ts. @@ -110,6 +112,10 @@ export const InputArea = ({ manager }: InputAreaProps) => {