Compare commits

..
3 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 7ce5af88aa todo: close the invisible spawned agent
The endpoint, table and ingest all existed and nothing called them, so the table
read empty and the feature looked unbuilt. Ingest on read means an empty table was
itself the symptom: nobody had looked.

The proposed second fix — the loop echoing the transcript id — turned out
unnecessary. The ids are already in the loop's spawned record, which the endpoint
ingests, so they reach the UI without the loop narrating anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:55:11 +00:00
pastilhasandClaude Opus 5 6bfc1d38f7 a spawned agent is a link, not a rumour
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/<id>` 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 <noreply@anthropic.com>
2026-08-23 04:54:54 +00:00
pastilhas c10dae3e6e scanner: text inside code, kbd, pre and samp is code, not prose
ApiKeys.tsx documents the header to send as <code>Authorization: Bearer …</code>.
That is an identifier crossing a boundary on the doc's never-translate list, and
translating it would break the thing it documents. Those four elements exist to mark
content as code, so the scanner now honours them.

Verified the delta across both remaining areas is exactly that one string.
2026-08-23 04:47:05 +00:00
4 changed files with 131 additions and 5 deletions
+10 -1
View File
@@ -104,6 +104,15 @@ function isHuman(value: string | undefined): value is string {
const stripComments = (source: string): string =>
source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '');
/**
* `<code>`, `<kbd>`, `<pre>` and `<samp>` mark content as code rather than prose — that is what the
* elements are for. `<code>Authorization: Bearer …</code>` is an HTTP header, which the doc's
* never-translate list covers as an identifier crossing a boundary, and translating it would break the
* thing it documents.
*/
const stripCodeElements = (source: string): string =>
source.replace(/<(code|kbd|pre|samp)\b[^>]*>[\s\S]*?<\/\1>/g, '<$1/>');
function* walk(dir: string): Generator<string> {
let entries: string[];
try {
@@ -135,7 +144,7 @@ function scan(root: string): { files: number; hits: Hit[] } {
files++;
let source: string;
try {
source = stripComments(readFileSync(file, 'utf8'));
source = stripCodeElements(stripComments(readFileSync(file, 'utf8')));
} catch {
continue;
}
@@ -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/<id>` 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 (
<div className="shrink-0 border-b border-border px-4 py-2">
<div className="flex items-baseline gap-2 text-xs text-muted-foreground">
<span className="font-medium uppercase tracking-wide">Spawned</span>
<span>{children.length}</span>
{/*
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 && (
<span>
· ${totalUsd.toFixed(2)}
{unknownCost ? '+' : ''}
</span>
)}
{unknownCost > 0 && (
<span title={`${unknownCost} child${unknownCost === 1 ? '' : 'ren'} did not report a cost`}>
· {unknownCost} unknown
</span>
)}
</div>
<div className="mt-1 flex flex-wrap gap-1.5">
{children.map((c) => (
<Link
key={c.spawnedSessionId}
to={`/chat/${c.spawnedSessionId}`}
title={`spawned ${new Date(c.firstSeenAt).toLocaleString()}${c.finishedAt ? `, finished ${new Date(c.finishedAt).toLocaleString()}` : ' — still running'}`}
className="rounded border border-border px-1.5 py-0.5 font-mono text-[11px] text-foreground/80 hover:border-primary/50 hover:text-foreground"
>
{c.spawnedSessionId.slice(0, 8)}
{!c.finishedAt && (
<span className="ml-1 inline-block h-1.5 w-1.5 rounded-full bg-duck-orange align-middle" />
)}
</Link>
))}
</div>
</div>
);
};
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}
/>
<SpawnedChildren sessionId={sessionId} />
<EmbeddableChat
chat={chat}
sessionId={undefined}
@@ -102,3 +102,39 @@ export function useCommandHqCommanderActions(sessionId: string) {
return { setCategory, rename, disarmWatcher, deleteCommander };
}
export type SpawnedChild = {
spawnedSessionId: string;
firstSeenAt: string;
finishedAt: string | null;
totalUsd: number | null;
outcome: string | null;
};
/**
* The agents this commander spawned.
*
* The backend ingests the loop's record file on every read, so calling this often is free and there is no
* timer to stop working quietly. Which is the reason this hook exists at all: the endpoint and the table
* were built, and nothing ever called them — so the table sat empty and a spawned agent stayed invisible
* even though its transcript was sitting there, readable, the whole time.
*/
export function useCommandHqChildren(sessionId: string | undefined) {
const client = useClient();
const { data, isLoading } = useQuery<{ children: SpawnedChild[]; totalUsd: number; unknownCost: number }>({
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,
};
}
@@ -2,7 +2,7 @@
id: background-jobs/a-spawned-agent-is-invisible-in-the-interface
title: 'An agent spawned by a background job is invisible in the interface'
theme: background-jobs
status: open
status: done
created: 2026-08-21
date_source: stated
order: 1
@@ -27,6 +27,25 @@ Two fixes, and the small one is most of the value:
against the commander that spawned it, which would also fix cost attribution — today the child's tokens
land nowhere, so a commander's cost line reads `$0.28` for a night that spent several dollars.
The second one needs a decision about what the child *is*. A commander is defined by its directory
The second one needs a decision about what the child _is_. A commander is defined by its directory
(`isCommandersDir`), and the child runs in the platform checkout, so it does not qualify — deliberately.
Making it visible probably means a different relationship than "another commander".
---
**Closed 2026-08-23.** The commander's pane lists its spawned agents, each a link to `/chat/<id>`, with a
dot on the ones still running.
Worth recording what the work actually turned out to be. The endpoint
(`GET /command-hq/commanders/:id/children`), the `command_hq_spawned` table and the ingest were all built
earlier the same night — and **nothing had ever called them**, so the table read empty and the feature looked
unbuilt. Ingest happens on read rather than on a timer, which means the table being empty was itself the
symptom: nobody had looked.
The second fix this file proposed — the loop echoing the transcript id — was not needed in the end. The ids
are already in the loop's `spawned` record, which the endpoint ingests, so the id reaches the UI without the
loop having to narrate it.
Cost attribution is partly addressed and honestly so: the pane shows a floor (`$1.20+`) with a count of
children that reported no cost, never a confident total. The remaining gap is that a child's spend does not
flow back to the commander's own cost line — that is cost accounting, deferred by the owner, and not this.