/** * 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 }), } }