Files
apes-sidebar/src/client/subagent-activity.ts
T

68 lines
2.8 KiB
TypeScript

/**
* Pure derivation of the compact LIVE line shown on a running subagent card:
* the last text output and the last tool call of the child's history tail
* (`subagent.history` events, oldest → newest). Renders nothing itself — the
* SubagentLiveLine component turns this into the card's status lines.
* Kept framework-free so the parser is unit-testable in the node environment.
*/
import type { SidebarHistoryEntry } from '../context-types.ts'
/**
* Extract the concatenated plain text of a content-block list (the durable
* `ContentBlock[]` shape, structurally: blocks with `type: 'text'` carry
* `text`; anything else — tool_use, image, … — contributes nothing).
* @param content - the raw `content` field of a message event.
* @returns the joined text, or undefined when the message carries no text.
*/
export function contentText(content: unknown): string | undefined {
if (!Array.isArray(content)) return undefined
const parts: string[] = []
for (const block of content) {
if (block === null || typeof block !== 'object') continue
const candidate = block as { type?: unknown; text?: unknown }
if (candidate.type === 'text' && typeof candidate.text === 'string') {
parts.push(candidate.text)
}
}
return parts.length > 0 ? parts.join('\n') : undefined
}
/** The live status of one subagent card (both fields optional). */
export interface LastActivity {
/** The latest assembled assistant text output in the tail. */
text?: string
/** The latest tool call in the tail. */
tool?: { name: string; args: string }
}
/**
* Fold a history tail into the last text output + last tool call (each is
* the LAST occurrence in event order). Lifecycle events and raw
* `assistant/chunk` rows are ignored — the card shows what the subagent is
* doing right now, not its plumbing.
* @param entries - the tail page from `subagent.history` (oldest → newest).
* @returns the last text and/or tool call; an empty object when the tail has neither.
*/
export function lastActivity(entries: SidebarHistoryEntry[]): LastActivity {
let text: string | undefined
let tool: { name: string; args: string } | undefined
for (const entry of entries) {
const { type, data } = entry.event
if (type === 'assistant/message') {
const message = data.message as { content?: unknown } | undefined
const extracted = contentText(message?.content)
if (extracted !== undefined) text = extracted
} else if (type === 'tool/call') {
tool = {
name: typeof data.name === 'string' ? data.name : 'tool',
args: typeof data.arguments === 'string' ? data.arguments : '',
}
}
}
if (text === undefined && tool === undefined) return {}
return {
...(text === undefined ? {} : { text }),
...(tool === undefined ? {} : { tool }),
}
}