From 6bfc1d38f712709890707f2d064d4f1d8244d9b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 23 Aug 2026 04:54:54 +0000 Subject: [PATCH] a spawned agent is a link, not a rumour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The endpoint, the table and the ingest all existed and nothing had ever called them — so `command_hq_spawned` sat empty and a spawned agent stayed invisible even though its transcript was readable the whole time. It writes an ordinary transcript under the project directory for its cwd, so `/chat/` has always rendered it live. What was missing was anybody being told the id existed: the job panel showed its output as a text stream in a small box with no link, so finding the session meant knowing to look. Now the commander's own pane lists them, each a link to its transcript, with a dot on the ones still running. Ingest happens on read, so the list is right without a timer that can stop working quietly. Cost is stated as a floor when any child did not report one — `$1.20+` and a count of unknowns, never a confident total. A commander's line reading $0.28 for a night that spent several dollars is the specific understatement this exists to stop. Renders nothing when there are none. A commander that has not fired yet is the normal case and does not need a box explaining that. Closes todo/background-jobs/a-spawned-agent-is-invisible-in-the-interface. Co-Authored-By: Claude Opus 5 --- .../src/apps/CommandHQ/CommandHqDetail.tsx | 66 ++++++++++++++++++- .../src/apps/CommandHQ/useCommandHq.ts | 36 ++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx index 3412011f..48a46507 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqDetail.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; -import { useNavigate, useParams, useSearchParams } from 'react-router'; +import { Link, 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'; @@ -12,7 +12,12 @@ import { useChat, EmbeddableChat } from '../Chat'; import { DirPickerModal } from '../ChatHistory/DirPickerModal'; import type { ChatMessage } from '../Chat/types'; import { categoryFromParam, commandHqListPath, commandHqSessionPath } from './command-hq-routes'; -import { useCommandHqCategories, useCommandHqCommander, useCommandHqCommanderActions } from './useCommandHq'; +import { + useCommandHqCategories, + useCommandHqChildren, + useCommandHqCommander, + useCommandHqCommanderActions, +} from './useCommandHq'; import { formatCommanderCost, formatTokens } from './format'; import { NEW_COMMANDER_GUIDE } from './newCommanderGuide'; @@ -390,6 +395,62 @@ function NewCommander({ ); } +/** + * The agents this commander spawned, each a link to its own transcript. + * + * A spawned agent has always been fully readable — it writes an ordinary transcript under the project + * directory for its cwd, and `/chat/` renders it live like any other session. What was missing was + * anybody being told the id existed. The job panel showed its output as a text stream in a small box with + * no link, so the only way to find the session was to know to go looking. + * + * Renders nothing when there are none, rather than an empty state. A commander that has not fired yet is + * the normal case and does not need a box explaining that. + */ +const SpawnedChildren = ({ sessionId }: { sessionId: string }) => { + const { children, totalUsd, unknownCost } = useCommandHqChildren(sessionId); + if (!children.length) return null; + + return ( +
+
+ Spawned + {children.length} + {/* + Cost is stated as a floor when any child's is unknown, never as a confident total. A commander's + line reading $0.28 for a night that spent several dollars is the specific understatement this + exists to stop. + */} + {totalUsd > 0 && ( + + · ${totalUsd.toFixed(2)} + {unknownCost ? '+' : ''} + + )} + {unknownCost > 0 && ( + + · {unknownCost} unknown + + )} +
+
+ {children.map((c) => ( + + {c.spawnedSessionId.slice(0, 8)} + {!c.finishedAt && ( + + )} + + ))} +
+
+ ); +}; + type MetaBarProps = { sessionId: string; title: string | undefined; @@ -576,6 +637,7 @@ function ResumedCommander({ sessionId, resume }: { sessionId: string; resume: Re isGenerating={chat.isGenerating} onDisconnect={chat.disconnectSession} /> + ({ + queryKey: ['command-hq', 'children', sessionId ?? ''], + queryFn: () => client.get(`/command-hq/commanders/${sessionId}/children`), + enabled: !!sessionId, + // A spawn appears while the panel is open, so a stale window here means watching a stale list during + // exactly the minutes somebody is watching. + staleTime: 0, + refetchInterval: 15_000, + retry: false, + }); + return { + children: data?.children ?? [], + totalUsd: data?.totalUsd ?? 0, + unknownCost: data?.unknownCost ?? 0, + isLoading, + }; +}