diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx index d87488b9..bbf7dadc 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx @@ -230,6 +230,23 @@ function NewCommander({ // updates nothing and says nothing. Same root cause as the navigate below. const { setCategory } = useCommandHqCommanderActions(chat.claudeSessionId ?? ''); const [filed, setFiled] = useState(false); + + /* + Has this commander been started — which is NOT the same as "has it any messages". + + `messages.length === 0` was standing in for it, and it lies in exactly the situation that matters. + Lose the socket mid-turn and the messages come back empty, so the setup pane and its Start button + reappear on a commander that is already running; pressing Start then sends the opening brief a + SECOND time, and the agent answers a question nobody asked twice. Seen tonight, after a restart I + ran while a commander was live. + + A latch, not a derived value. Once started, always started — the point is to survive the moment the + live state goes empty, so anything recomputed from that state is the bug rather than the fix. + */ + const [started, setStarted] = useState(false); + useEffect(() => { + if (chat.messages.length > 0 || chat.claudeSessionId || chat.isGenerating) setStarted(true); + }, [chat.messages.length, chat.claudeSessionId, chat.isGenerating]); /* Refresh the grid when the first turn ends. @@ -313,7 +330,7 @@ function NewCommander({ So the scroller is here, where the height is actually constrained, and the composer below keeps `shrink-0` so it stays reachable at the bottom whatever the zoom. */} - {chat.messages.length === 0 && ( + {!started && (