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, + }; +}