import { getMarkdownTheme, ToolExecutionComponent, type ExtensionAPI, } from "@earendil-works/pi-coding-agent"; import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"; import { normalizeDisplaySummary } from "./display-summary.js"; import { onReloadShutdown } from "./extension-lifecycle.js"; import { shortenPath } from "./render-utils.js"; export type AggregateMemberState = | "pending" | "running" | "success" | "failed" | "needsAttention"; export interface AggregateMember { toolCallId: string; toolName: string; groupId: string; sourceOrder: number; args: Record; state: AggregateMemberState; errorSummary?: string; visible: boolean; retainedDone?: boolean; completionOrder?: number; } export interface AggregateUsageTotals { input: number; output: number; cacheRead: number; cacheWrite: number; } export interface AggregateSteer { id: string; text: string; firstLine: string; } export interface AggregateGroup { groupId: string; leaderToolCallId?: string; members: AggregateMember[]; framedItemIds: string[]; narrationById: Map; agentTurnIds: string[]; usageByKey: Map; steers: AggregateSteer[]; hasSeenToolBatch: boolean; settled: boolean; startedAtMs?: number; endedAtMs?: number; } export type AggregateFrameEdge = "start" | "continue" | "end" | "only"; export interface AggregateToolSummary { toolName: string; count: number; lastTarget: string; } export interface AggregateActivityView { groupId: string; leaderToolCallId: string; hasRunning: boolean; latestNarration?: string; callCount: number; agentTurnCount: number; settled: boolean; durationMs?: number; completedAtMs?: number; usage?: AggregateUsageTotals; active: AggregateMember[]; displayRows: AggregateMember[]; activeOverflow: number; failed: AggregateMember[]; failedCount: number; steerCount: number; pinnedSteers: Array<{ id: string; firstLine: string }>; toolSummaries: AggregateToolSummary[]; } export interface AggregateRenderTheme { fg(color: string, text: string): string; bold?(text: string): string; } interface ToolCallRecord { id: string; name: string; args: Record; } interface SessionContextLike { hasUI?: boolean; sessionManager?: { getBranch(): unknown[]; buildSessionContext?(): { messages?: unknown[] }; }; } interface PatchableToolExecution { toolName?: unknown; toolCallId?: unknown; args?: unknown; expanded?: unknown; result?: unknown; ui?: { requestRender?: () => void }; invalidate?: () => void; } interface FrameInvalidator { id: string; invalidate: () => void; } interface PatchableToolExecutionPrototype { render(width: number): string[]; [AGGREGATE_TOOL_EXECUTION_PATCH_KEY]?: AggregateToolExecutionPatchState; } interface AggregateToolExecutionPatchState { originalRender: (this: PatchableToolExecution, width: number) => string[]; patchedRender: (this: PatchableToolExecution, width: number) => string[]; projection?: AggregateProjection; } const FAILED_SUMMARY_MAX_LENGTH = 200; const ACTIVE_ROW_LIMIT = 3; const AGGREGATE_FRAME_CONTINUE = " │ "; const AGGREGATE_FRAME_END = " └ "; export const AGGREGATE_ASSISTANT_MARK = "›"; export const AGGREGATE_STEER_MARK = "↳"; const COLLAPSED_NARRATION_ROW_LIMIT = 3; const COLLAPSED_NARRATION_SOURCE_MAX_LENGTH = 2_000; const OSC_SEQUENCE_PATTERN = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g; const ANSI_SEQUENCE_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]/g; const NARRATION_CONTROL_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g; export const AGGREGATE_DONE_SETTLE_DELAY_MS = 1_500; export const DEFAULT_AGGREGATE_RENDER_PASSTHROUGH = ["Agent"] as const; const AGGREGATE_TOOL_EXECUTION_PATCH_KEY = Symbol.for( "pi-tool-display-intent.aggregate-tool-execution.v1", ); const registeredApis = new WeakSet(); const TOOL_COLOR_PALETTE = [ "mdLink", "syntaxString", "syntaxFunction", "accent", "bashMode", "customMessageLabel", "syntaxType", ] as const; const PLAIN_THEME: AggregateRenderTheme = { fg: (_color, text) => text, bold: (text) => text, }; function publicThemeFallback(): AggregateRenderTheme { try { const markdown = getMarkdownTheme(); const palette = [ markdown.link, markdown.code, markdown.heading, markdown.codeBlock, markdown.listBullet, ]; return { fg(color, text) { if (color === "muted" || color === "dim") return markdown.quote(text); if (color === "success") return markdown.codeBlock(text); if (color === "warning" || color === "error" || color === "accent") return markdown.heading(text); if (color === "toolTitle") return markdown.code(text); let hash = 0; for (const character of color) hash = (hash * 31 + character.codePointAt(0)!) >>> 0; return palette[hash % palette.length]!(text); }, bold: markdown.bold, }; } catch { return PLAIN_THEME; } } function toRecord(value: unknown): Record { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; } function normalizeToolName(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const normalized = value.trim(); return normalized || undefined; } function textContent(result: unknown): string { const content = toRecord(result).content; if (!Array.isArray(content)) return ""; return content .filter((entry) => toRecord(entry).type === "text") .map((entry) => String(toRecord(entry).text ?? "")) .join("\n"); } export function aggregateResultHasImage(result: unknown): boolean { const content = toRecord(result).content; return Array.isArray(content) && content.some((entry) => toRecord(entry).type === "image"); } function firstMeaningfulLine(value: unknown, fallback: string): string { for (const line of textContent(value).replace(/\r/g, "").split("\n")) { const normalized = normalizeDisplaySummary(line, FAILED_SUMMARY_MAX_LENGTH); if (normalized) return normalized; } return fallback; } function getPath(args: unknown): string | undefined { const record = toRecord(args); const value = record.path ?? record.file_path; return typeof value === "string" && value.trim() ? value : undefined; } function normalizeTargetText(value: unknown, fallback: string): string { return normalizeDisplaySummary(value, 400) ?? fallback; } function formatAggregatePath(args: unknown): string { return normalizeTargetText(shortenPath(getPath(args) ?? "."), "."); } export function formatAggregateTarget( member: Pick, ): string { const args = member.args; const path = formatAggregatePath(args); switch (member.toolName) { case "read": return `Read(${path})`; case "grep": { const pattern = normalizeTargetText(args.pattern, "pattern"); return `Search(/${pattern}/ in ${path})`; } case "find": return `Find(${normalizeTargetText(args.pattern, "pattern")} in ${path})`; case "ls": return `List(${path})`; case "bash": return `Bash(${normalizeTargetText(args.command, "command")})`; case "edit": return `Edit(${path})`; case "write": return `Write(${path})`; default: return member.toolName; } } function toolColor(toolName: string): string { let hash = 0; for (const character of toolName) hash = (hash * 31 + character.codePointAt(0)!) >>> 0; return TOOL_COLOR_PALETTE[hash % TOOL_COLOR_PALETTE.length]!; } function formatColoredTarget( member: Pick, theme: AggregateRenderTheme, ): string { return theme.fg(toolColor(member.toolName), formatAggregateTarget(member)); } function messageRole(value: unknown): string | undefined { const role = toRecord(value).role; return typeof role === "string" ? role : undefined; } function messageContent(value: unknown): unknown[] { const content = toRecord(value).content; return Array.isArray(content) ? content : []; } function toolCallsFromMessage(value: unknown): ToolCallRecord[] { return messageContent(value).flatMap((entry) => { const content = toRecord(entry); const name = normalizeToolName(content.name); if (content.type !== "toolCall" || typeof content.id !== "string" || !name) return []; return [{ id: content.id, name, args: toRecord(content.arguments) }]; }); } function messageHasVisibleText(value: unknown): boolean { return firstVisibleAssistantText(value) !== undefined; } export function normalizeAssistantNarration(value: unknown): string | undefined { if (typeof value !== "string") return undefined; const sanitized = value .replace(OSC_SEQUENCE_PATTERN, "") .replace(ANSI_SEQUENCE_PATTERN, "") .replace(NARRATION_CONTROL_PATTERN, "") .replace(/\r\n/g, "\n") .replace(/\r/g, "\n") .replace(/[ \t]+\n/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); if (!sanitized) return undefined; return sanitized.length > COLLAPSED_NARRATION_SOURCE_MAX_LENGTH ? sanitized.slice(0, COLLAPSED_NARRATION_SOURCE_MAX_LENGTH) : sanitized; } function firstVisibleAssistantText(value: unknown): string | undefined { for (const entry of messageContent(value)) { const content = toRecord(entry); if (content.type !== "text" || typeof content.text !== "string") continue; const normalized = normalizeAssistantNarration(content.text); if (normalized) return normalized; } return undefined; } function isVisuallyBlank(line: string): boolean { return visibleWidth(line) === 0; } function trimRenderedEdges(lines: readonly string[]): string[] { const kept = [...lines]; while (kept.length > 0 && isVisuallyBlank(kept[0]!)) kept.shift(); while (kept.length > 0 && isVisuallyBlank(kept[kept.length - 1]!)) kept.pop(); return kept; } function renderNarrationMarkdownLines(text: string, width: number): string[] { try { const lines = trimRenderedEdges(new Markdown(text, 0, 0, getMarkdownTheme()).render(width)); if (lines.length > 0) return lines; } catch { // Public markdown fallbacks and unbound Pi theme helpers must not crash the ledger. } const wrapped = wrapTextWithAnsi(text.replace(/\s+/g, " ").trim(), width); return wrapped.length > 0 ? wrapped : [text]; } function colorSteerText(theme: AggregateRenderTheme, text: string): string { try { return theme.fg("accent", text); } catch { return text; } } export function formatAggregateSteerCount(count: number): string { return `${count} ${count === 1 ? "steer" : "steers"}`; } export function renderCollapsedSteerPins( steers: ReadonlyArray<{ firstLine: string }>, width: number, theme: AggregateRenderTheme, ): string[] { const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0; if (safeWidth === 0 || steers.length === 0) return []; return steers.map((steer) => truncateToWidth( ` ${colorSteerText(theme, `${AGGREGATE_STEER_MARK} ${steer.firstLine}`)}`, safeWidth, "…", ), ); } export function renderSettledSteerReminder( steerCount: number, width: number, theme: AggregateRenderTheme, ): string[] { const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0; if (safeWidth === 0 || steerCount <= 0) return []; return [ truncateToWidth( ` ${colorSteerText(theme, `${AGGREGATE_STEER_MARK} ${formatAggregateSteerCount(steerCount)}`)}`, safeWidth, "…", ), ]; } export function renderExpandedAggregateSteer( text: string, width: number, theme: AggregateRenderTheme, edge: AggregateFrameEdge = "only", ): string[] { const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); while (lines.length > 0 && !lines[0]!.trim()) lines.shift(); while (lines.length > 0 && !lines[lines.length - 1]!.trim()) lines.pop(); const body = lines.length > 0 ? lines : [""]; const marked = [ "", ...body.map((line, index) => index === 0 ? colorSteerText(theme, `${AGGREGATE_STEER_MARK} ${line}`) : line), "", ]; return applyAggregateGroupFrame(marked, width, theme, edge); } export function renderCollapsedAssistantNarration( text: string, width: number, theme: AggregateRenderTheme, ): string[] { const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0; if (safeWidth === 0 || !text) return []; let mark = AGGREGATE_ASSISTANT_MARK; try { mark = theme.fg("muted", AGGREGATE_ASSISTANT_MARK); } catch { // Theme helpers must not crash the collapsed ledger. } const prefix = ` ${mark} `; const continuation = " "; const contentWidth = Math.max(1, safeWidth - visibleWidth(prefix)); const rows = renderNarrationMarkdownLines(text, contentWidth).slice(0, COLLAPSED_NARRATION_ROW_LIMIT); return rows.map((row, index) => { const linePrefix = index === 0 ? prefix : continuation; return truncateToWidth(`${linePrefix}${row}`, safeWidth, "…"); }); } function isInterimAssistantMessage(value: unknown): boolean { const reason = toRecord(value).stopReason; if (reason === "error" || reason === "aborted" || reason === "length" || reason === "stop") return false; return reason === "toolUse" || toolCallsFromMessage(value).length > 0; } export function aggregateAssistantFrameId(message: unknown): string | undefined { const firstToolId = toolCallsFromMessage(message)[0]?.id; if (firstToolId) return `assistant-before:${firstToolId}`; const record = toRecord(message); if (typeof record.id === "string" && record.id.trim()) return `assistant:${record.id}`; if (typeof record.timestamp === "number") return `assistant:${record.timestamp}`; return undefined; } export function aggregateAssistantTurnId(message: unknown): string | undefined { const record = toRecord(message); if (typeof record.id === "string" && record.id.trim()) return `assistant:${record.id}`; if (typeof record.timestamp === "number") return `assistant:${record.timestamp}`; return undefined; } function collectVisibleToolCallIds(messages: unknown[] | undefined): Set | undefined { if (!Array.isArray(messages)) return undefined; const ids = new Set(); for (const message of messages) { for (const call of toolCallsFromMessage(message)) ids.add(call.id); } return ids; } function entryMessage(entry: unknown): unknown | undefined { const record = toRecord(entry); return record.type === "message" ? record.message : undefined; } function entryId(entry: unknown, fallback: string): string { const id = toRecord(entry).id; return typeof id === "string" ? id : fallback; } function isAssistantTerminalFailure(message: unknown): boolean { const reason = toRecord(message).stopReason; return reason === "aborted" || reason === "error"; } function isAssistantTerminal(message: unknown): boolean { const reason = toRecord(message).stopReason; return reason === "stop" || reason === "error" || reason === "aborted" || reason === "length"; } export function userMessageText(message: unknown): string { const content = toRecord(message).content; if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .filter((entry) => toRecord(entry).type === "text") .map((entry) => String(toRecord(entry).text ?? "")) .join(""); } export function steerFirstLine(text: string): string { const sanitized = text .replace(OSC_SEQUENCE_PATTERN, "") .replace(ANSI_SEQUENCE_PATTERN, "") .replace(NARRATION_CONTROL_PATTERN, "") .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); for (const line of sanitized.split("\n")) { const trimmed = line.trim(); if (trimmed) return trimmed; } return ""; } function parseTimestampMs(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value; if (typeof value === "string" && value.trim()) { const parsed = Date.parse(value); if (Number.isFinite(parsed)) return parsed; } return undefined; } function messageTimestampMs(value: unknown, fallback?: unknown): number | undefined { return parseTimestampMs(toRecord(value).timestamp) ?? parseTimestampMs(fallback); } function emptyUsage(): AggregateUsageTotals { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; } function usageFromUnknown(value: unknown): AggregateUsageTotals | undefined { const usage = toRecord(toRecord(value).usage); const input = typeof usage.input === "number" && Number.isFinite(usage.input) ? usage.input : 0; const output = typeof usage.output === "number" && Number.isFinite(usage.output) ? usage.output : 0; const cacheRead = typeof usage.cacheRead === "number" && Number.isFinite(usage.cacheRead) ? usage.cacheRead : 0; const cacheWrite = typeof usage.cacheWrite === "number" && Number.isFinite(usage.cacheWrite) ? usage.cacheWrite : 0; if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return undefined; return { input, output, cacheRead, cacheWrite }; } function sumUsage(usageByKey: Map): AggregateUsageTotals | undefined { const totals = emptyUsage(); let hasUsage = false; for (const usage of usageByKey.values()) { hasUsage = true; totals.input += usage.input; totals.output += usage.output; totals.cacheRead += usage.cacheRead; totals.cacheWrite += usage.cacheWrite; } return hasUsage ? totals : undefined; } export function formatCompactTokenCount(count: number): string { if (count < 1000) return String(count); if (count < 10_000) return `${(count / 1000).toFixed(1)}k`; if (count < 1_000_000) return `${Math.round(count / 1000)}k`; if (count < 10_000_000) return `${(count / 1_000_000).toFixed(1)}M`; return `${Math.round(count / 1_000_000)}M`; } export function formatAggregateDuration(ms: number): string { const totalSeconds = Math.max(0, Math.round(ms / 1000)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours > 0) return `${hours}h${minutes}m${seconds}s`; if (minutes > 0) return `${minutes}m${seconds}s`; return `${seconds}s`; } export function formatAggregateClock(ms: number): string { const date = new Date(ms); const year = String(date.getFullYear()); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } export function formatAggregateStatsLine( view: Pick, ): string | undefined { if (!view.settled) return undefined; const parts: string[] = []; if (typeof view.durationMs === "number") parts.push(`took ${formatAggregateDuration(view.durationMs)}`); if (view.usage) { const tokenParts: string[] = []; if (view.usage.input) tokenParts.push(`↑${formatCompactTokenCount(view.usage.input)}`); if (view.usage.output) tokenParts.push(`↓${formatCompactTokenCount(view.usage.output)}`); if (view.usage.cacheRead) tokenParts.push(`R${formatCompactTokenCount(view.usage.cacheRead)}`); if (view.usage.cacheWrite) tokenParts.push(`W${formatCompactTokenCount(view.usage.cacheWrite)}`); if (tokenParts.length > 0) parts.push(`tok ${tokenParts.join(" ")}`); } if (typeof view.completedAtMs === "number") parts.push(`at ${formatAggregateClock(view.completedAtMs)}`); return parts.length > 0 ? parts.join(" · ") : undefined; } function assistantFailureSummary(message: unknown): string { const record = toRecord(message); if (record.stopReason === "aborted") return "Operation aborted."; return normalizeDisplaySummary(record.errorMessage, FAILED_SUMMARY_MAX_LENGTH) ?? "Assistant turn failed."; } const projectionsByOwner = new WeakMap(); const liveProjections = new Set(); let hostAggregateProjection: AggregateProjection | undefined; function rememberProjection(owner: object | undefined, projection: AggregateProjection): void { liveProjections.add(projection); if (owner) projectionsByOwner.set(owner, projection); } function forgetProjection(owner: object | undefined, projection: AggregateProjection): void { liveProjections.delete(projection); if (owner) projectionsByOwner.delete(owner); if (hostAggregateProjection === projection) hostAggregateProjection = undefined; } function claimHostProjection(owner: object | undefined, projection: AggregateProjection): boolean { rememberProjection(owner, projection); if (!hostAggregateProjection) { hostAggregateProjection = projection; return true; } return hostAggregateProjection === projection; } export function getActiveAggregateProjection(): AggregateProjection | undefined { return hostAggregateProjection; } export function resolveAggregateProjection( preferred?: AggregateProjection, ...hints: unknown[] ): AggregateProjection | undefined { if (preferred) return preferred; for (const hint of hints) { if (typeof hint !== "string" || !hint) continue; for (const projection of liveProjections) { if (projection.getMember(hint) || projection.getFrameEdge(hint)) return projection; } } return hostAggregateProjection; } export function resolveAggregateRenderTheme(preferred?: AggregateProjection): AggregateRenderTheme { return preferred?.getRenderTheme() ?? hostAggregateProjection?.getRenderTheme() ?? publicThemeFallback(); } export class AggregateProjection { private readonly groups: AggregateGroup[] = []; private readonly groupsById = new Map(); private readonly membersById = new Map(); private readonly framedGroupById = new Map(); private readonly visibleFrameContent = new Set(); private readonly frameInvalidators: FrameInvalidator[] = []; private readonly invalidators = new Map void>(); private readonly assignedSteerIds = new Set(); private readonly steersByInstance = new WeakMap(); private sourceOrder = 0; private completionOrder = 0; private liveGroupSequence = 0; private activeGroupId: string | undefined; private initialized = false; private renderTheme: AggregateRenderTheme | undefined; constructor(private readonly isPassthroughTool: (toolName: string) => boolean = () => false) {} isInitialized(): boolean { return this.initialized; } isPassthrough(toolName: string): boolean { return this.isPassthroughTool(toolName); } setRenderTheme(theme: AggregateRenderTheme): void { this.renderTheme = theme; } getRenderTheme(): AggregateRenderTheme { return this.renderTheme ?? publicThemeFallback(); } getGroups(): readonly AggregateGroup[] { return this.groups; } hasPaintedToolsLedger(message?: unknown): boolean { const turnId = aggregateAssistantTurnId(message); if (turnId) { for (const group of this.groups) { if (group.agentTurnIds.includes(turnId)) return Boolean(group.leaderToolCallId); } } const active = this.activeGroupId ? this.groupsById.get(this.activeGroupId) : undefined; return Boolean(active?.leaderToolCallId); } getMember(toolCallId: string): AggregateMember | undefined { return this.membersById.get(toolCallId); } getFrameEdge(itemId: string): AggregateFrameEdge | undefined { const items = this.getFramedItemIds(itemId); const index = items.indexOf(itemId); if (index < 0) return undefined; if (items.length === 1) return "only"; if (index === 0) return "start"; if (index === items.length - 1) return "end"; return "continue"; } getFramedItemIds(itemId: string): string[] { const groupId = this.framedGroupById.get(itemId); return groupId ? [...(this.groupsById.get(groupId)?.framedItemIds ?? [])] : []; } isFrameStart(itemId: string): boolean { const edge = this.getFrameEdge(itemId); return edge === "start" || edge === "only"; } shouldHostExpandedSummary(itemId: string): boolean { const items = this.getFramedItemIds(itemId); const firstVisible = items.find((id) => this.hasVisibleFrameContent(id)); return firstVisible === itemId; } hasVisibleFrameContent(itemId: string): boolean { if ( !itemId.startsWith("assistant-before:") && !itemId.startsWith("assistant:") && !itemId.startsWith("steer:") ) { return true; } return this.visibleFrameContent.has(itemId); } markFrameContentVisible(itemId: string, visible: boolean): void { if (!itemId) return; const previousHost = this.getFramedItemIds(itemId).find((id) => this.hasVisibleFrameContent(id)); if (visible) this.visibleFrameContent.add(itemId); else this.visibleFrameContent.delete(itemId); const nextHost = this.getFramedItemIds(itemId).find((id) => this.hasVisibleFrameContent(id)); if (previousHost !== nextHost) this.invalidateIds(previousHost, nextHost, itemId); } getViewForGroup(itemId: string): AggregateActivityView | undefined { const groupId = this.framedGroupById.get(itemId) ?? this.membersById.get(itemId)?.groupId; if (!groupId) return undefined; const group = this.groupsById.get(groupId); if (!group?.leaderToolCallId) return undefined; return this.getView(group.leaderToolCallId); } private groupIdForFrameItem(itemId: string, beforeId?: string): string | undefined { if (beforeId) { const fromTool = this.membersById.get(beforeId)?.groupId ?? this.framedGroupById.get(beforeId); if (fromTool) return fromTool; } const existing = this.framedGroupById.get(itemId); if (existing) return existing; const prefix = "assistant-before:"; if (itemId.startsWith(prefix)) { const toolId = itemId.slice(prefix.length); return this.membersById.get(toolId)?.groupId ?? this.framedGroupById.get(toolId); } return this.activeGroupId; } trackFramedItem(itemId: string, groupId?: string, beforeId?: string): void { const resolvedGroupId = groupId ?? this.groupIdForFrameItem(itemId, beforeId); if (!resolvedGroupId || !itemId) return; const existingGroupId = this.framedGroupById.get(itemId); if (existingGroupId === resolvedGroupId) return; if (existingGroupId) this.untrackFramedItem(itemId); const group = this.ensureGroup(resolvedGroupId); const previousLast = group.framedItemIds[group.framedItemIds.length - 1]; const beforeIndex = beforeId ? group.framedItemIds.indexOf(beforeId) : -1; if (beforeIndex >= 0) group.framedItemIds.splice(beforeIndex, 0, itemId); else group.framedItemIds.push(itemId); this.framedGroupById.set(itemId, resolvedGroupId); this.invalidateIds(previousLast, itemId); } untrackFramedItem(itemId: string): void { const groupId = this.framedGroupById.get(itemId); if (!groupId) return; const group = this.groupsById.get(groupId); const previousLast = group?.framedItemIds[group.framedItemIds.length - 1]; if (group) group.framedItemIds = group.framedItemIds.filter((id) => id !== itemId); this.framedGroupById.delete(itemId); this.invalidateIds(previousLast, group?.framedItemIds[group.framedItemIds.length - 1]); } connectFrameRenderer(itemId: string, invalidate: (() => void) | undefined): void { if (!invalidate || !itemId) return; if (this.frameInvalidators.some((entry) => entry.id === itemId && entry.invalidate === invalidate)) return; this.frameInvalidators.push({ id: itemId, invalidate }); } rememberNarration(itemId: string, text: string, groupId?: string): void { const resolvedGroupId = groupId ?? this.groupIdForFrameItem(itemId); if (!resolvedGroupId || !itemId || !text) return; const group = this.ensureGroup(resolvedGroupId); group.narrationById.set(itemId, text); this.invalidateIds(group.leaderToolCallId); } rememberAgentTurn(message: unknown): void { const group = this.ensureActiveGroup(); const id = aggregateAssistantTurnId(message) ?? `assistant-turn:${group.agentTurnIds.length + 1}`; if (!group.agentTurnIds.includes(id)) group.agentTurnIds.push(id); this.rememberUsage(id, message); this.rememberEndedAt(messageTimestampMs(message)); } rememberUsage(key: string, value: unknown): void { const usage = usageFromUnknown(value); if (!key || !usage) return; const group = this.ensureActiveGroup(); group.usageByKey.set(key, usage); this.invalidateIds(group.leaderToolCallId); } rememberStartedAt(timestampMs: number | undefined): void { if (timestampMs === undefined) return; const group = this.ensureActiveGroup(); if (group.startedAtMs === undefined || timestampMs < group.startedAtMs) group.startedAtMs = timestampMs; this.invalidateIds(group.leaderToolCallId); } rememberEndedAt(timestampMs: number | undefined): void { if (timestampMs === undefined) return; this.rememberStartedAt(timestampMs); const group = this.ensureActiveGroup(); if (group.endedAtMs === undefined || timestampMs > group.endedAtMs) group.endedAtMs = timestampMs; this.invalidateIds(group.leaderToolCallId); } markGroupSettled(groupId = this.activeGroupId, endedAtMs?: number): void { if (!groupId) return; const group = this.groupsById.get(groupId); if (!group || group.settled) return; group.settled = true; this.rememberEndedAt(endedAtMs ?? (group.endedAtMs === undefined ? Date.now() : undefined)); this.invalidateIds(group.leaderToolCallId); } latestNarrationFor(itemId: string): string | undefined { const groupId = this.framedGroupById.get(itemId) ?? this.membersById.get(itemId)?.groupId; if (!groupId) return undefined; const group = this.groupsById.get(groupId); if (!group) return undefined; for (const frameId of [...group.framedItemIds].reverse()) { const narration = group.narrationById.get(frameId); if (narration) return narration; } return undefined; } getConnectedRendererCount(): number { return this.invalidators.size; } startUserGroup(groupId?: string, startedAtMs?: number, options: { collapseRetainedDone?: boolean } = {}): string { const previousGroupId = this.activeGroupId; if (previousGroupId && previousGroupId !== groupId) { this.markUnsettledInterrupted(); this.markGroupSettled(previousGroupId, startedAtMs); } if (options.collapseRetainedDone !== false) this.collapseRetainedDone(); const resolvedId = groupId || `live-user-${++this.liveGroupSequence}`; const group = this.ensureGroup(resolvedId); this.activeGroupId = resolvedId; this.initialized = true; const fromId = resolvedId.startsWith("live-user-") ? parseTimestampMs(resolvedId.slice("live-user-".length)) : undefined; this.rememberStartedAt(startedAtMs ?? fromId); group.settled = false; return resolvedId; } shouldTreatAsSteer(streamingBehavior?: "steer" | "followUp"): boolean { const group = this.activeGroupId ? this.groupsById.get(this.activeGroupId) : undefined; if (!group || group.settled) return false; if (streamingBehavior === "followUp") return false; if (streamingBehavior === "steer") return true; return group.hasSeenToolBatch; } recordSteer(text: string, timestampMs?: number): string | undefined { const group = this.activeGroupId ? this.groupsById.get(this.activeGroupId) : undefined; if (!group) return undefined; const id = `steer:${group.groupId}:${group.steers.length}`; group.steers.push({ id, text, firstLine: steerFirstLine(text), }); this.trackFramedItem(id, group.groupId); this.rememberEndedAt(timestampMs); this.invalidateIds(group.leaderToolCallId); return id; } ingestUserMessage( message: unknown, options: { streamingBehavior?: "steer" | "followUp"; collapseRetainedDone?: boolean; groupId?: string; timestampMs?: number; } = {}, ): "steer" | "group" | undefined { if (messageRole(message) !== "user") return undefined; const timestampMs = options.timestampMs ?? messageTimestampMs(message); if (this.shouldTreatAsSteer(options.streamingBehavior)) { this.recordSteer(userMessageText(message), timestampMs); return "steer"; } const groupId = options.groupId ?? (timestampMs !== undefined ? `live-user-${timestampMs}` : undefined); this.startUserGroup(groupId, timestampMs, { collapseRetainedDone: options.collapseRetainedDone, }); return "group"; } getSteer(id: string): AggregateSteer | undefined { for (const group of this.groups) { const found = group.steers.find((steer) => steer.id === id); if (found) return found; } return undefined; } matchSteerForComponent(component: object, text?: string): AggregateSteer | undefined { const existingId = this.steersByInstance.get(component); if (existingId) { const existing = this.getSteer(existingId); if (existing) return existing; } if (text === undefined) return undefined; for (const group of this.groups) { for (const steer of group.steers) { if (steer.text !== text || this.assignedSteerIds.has(steer.id)) continue; this.assignedSteerIds.add(steer.id); this.steersByInstance.set(component, steer.id); return steer; } } for (const group of this.groups) { const found = group.steers.find((steer) => steer.text === text); if (found) return found; } return undefined; } connectRenderer( toolCallId: string, toolName: string, args: unknown, invalidate: (() => void) | undefined, ): void { if (!this.initialized) { if (invalidate) this.invalidators.set(toolCallId, invalidate); return; } const member = this.membersById.get(toolCallId); if (!member || !member.visible) return; if (invalidate) this.invalidators.set(toolCallId, invalidate); member.args = { ...member.args, ...toRecord(args) }; if (member.toolName !== toolName) member.toolName = toolName; } ingestAssistantMessage(message: unknown): void { if (messageRole(message) !== "assistant") return; this.rememberAgentTurn(message); const calls = toolCallsFromMessage(message); if (calls.length > 0) this.markGroupSawToolBatch(); if (isInterimAssistantMessage(message)) { const frameId = aggregateAssistantFrameId(message); const narration = firstVisibleAssistantText(message); if (frameId && narration) { this.trackFramedItem(frameId, this.activeGroupId, calls[0]?.id); this.rememberNarration(frameId, narration); } } for (const call of calls) { this.addOrUpdateMember(call.id, call.name, call.args, true); } if (isAssistantTerminalFailure(message)) { const summary = assistantFailureSummary(message); for (const call of toolCallsFromMessage(message)) { if (this.membersById.has(call.id)) this.markFailed(call.id, summary); } } this.maybeSettleFromTerminalAssistant(message); } ingestToolResult( message: unknown, options: { retainDone?: boolean; fallbackTimestamp?: unknown } = {}, ): void { if (messageRole(message) !== "toolResult") return; const record = toRecord(message); if (typeof record.toolCallId === "string") { const member = this.membersById.get(record.toolCallId); if (!member || !this.isPassthrough(member.toolName)) { this.rememberUsage(`tool:${record.toolCallId}`, message); } this.markComplete(record.toolCallId, record, record.isError === true, { retainDone: options.retainDone, }); } this.markGroupSawToolBatch(this.membersById.get(String(record.toolCallId))?.groupId); this.rememberEndedAt(messageTimestampMs(message, options.fallbackTimestamp)); } markStarted(toolCallId: string, toolName: string, args: unknown): void { const normalizedName = normalizeToolName(toolName); if (!normalizedName) return; const member = this.addOrUpdateMember(toolCallId, normalizedName, args, true); if (!member || member.state === "needsAttention") return; member.state = "running"; member.retainedDone = false; member.completionOrder = undefined; const group = this.groupsById.get(member.groupId); if (group) group.settled = false; this.invalidateGroup(member.groupId, toolCallId); } markUpdated(toolCallId: string, args: unknown): void { const member = this.membersById.get(toolCallId); if (!member) return; member.args = { ...member.args, ...toRecord(args) }; if (member.state === "pending") member.state = "running"; this.invalidateGroup(member.groupId, toolCallId); } markComplete( toolCallId: string, result: unknown, isError: boolean, options: { retainDone?: boolean } = {}, ): void { const member = this.membersById.get(toolCallId); if (!member) return; if (aggregateResultHasImage(result)) { this.markNeedsAttention(toolCallId); return; } if (isError) { this.markFailed(toolCallId, firstMeaningfulLine(result, "Tool failed.")); return; } const firstSuccess = member.state !== "success"; member.state = "success"; member.errorSummary = undefined; if (firstSuccess && options.retainDone !== false && !this.isPassthrough(member.toolName)) { member.retainedDone = true; member.completionOrder = ++this.completionOrder; this.trimRetainedDone(member.groupId); } this.invalidateGroup(member.groupId, toolCallId); } markNeedsAttention(toolCallId: string): void { const member = this.membersById.get(toolCallId); if (!member || member.state === "needsAttention") return; member.state = "needsAttention"; member.errorSummary = undefined; member.retainedDone = false; member.completionOrder = undefined; this.recomputeLeader(member.groupId); this.invalidateGroup(member.groupId, toolCallId); } markFailed(toolCallId: string, summary: string): void { const member = this.membersById.get(toolCallId); if (!member || member.state === "needsAttention") return; member.state = "failed"; member.errorSummary = normalizeDisplaySummary(summary, FAILED_SUMMARY_MAX_LENGTH) ?? "Tool failed."; member.retainedDone = false; member.completionOrder = undefined; this.invalidateGroup(member.groupId, toolCallId); } collapseRetainedDone(): void { const changedGroups = new Set(); for (const member of this.membersById.values()) { if (!member.retainedDone) continue; member.retainedDone = false; member.completionOrder = undefined; changedGroups.add(member.groupId); } for (const groupId of changedGroups) this.invalidateGroup(groupId); } markUnsettledInterrupted(summary = "Interrupted before a final result."): void { for (const member of this.membersById.values()) { if (member.state === "pending" || member.state === "running") { this.markFailed(member.toolCallId, summary); } } } rebuild(branchEntries: unknown[], visibleMessages?: unknown[]): void { const visibleIds = collectVisibleToolCallIds(visibleMessages); this.groups.length = 0; this.groupsById.clear(); this.membersById.clear(); this.framedGroupById.clear(); this.visibleFrameContent.clear(); this.frameInvalidators.length = 0; this.assignedSteerIds.clear(); this.sourceOrder = 0; this.completionOrder = 0; this.activeGroupId = undefined; let fallbackGroupIndex = 0; for (const entry of Array.isArray(branchEntries) ? branchEntries : []) { const message = entryMessage(entry); if (!message) continue; const role = messageRole(message); if (role === "user") { const restoredId = entryId(entry, `restored-user-${++fallbackGroupIndex}`); this.ingestUserMessage(message, { groupId: restoredId, collapseRetainedDone: false, timestampMs: messageTimestampMs(message, toRecord(entry).timestamp), }); continue; } if (role === "assistant") { this.ingestAssistantMessage(message); this.rememberEndedAt(messageTimestampMs(message, toRecord(entry).timestamp)); for (const call of toolCallsFromMessage(message)) { const member = this.membersById.get(call.id); if (!member) continue; const visible = visibleIds?.has(call.id) ?? true; member.visible = visible; if (!visible) this.untrackFramedItem(call.id); } continue; } if (role === "toolResult") { this.ingestToolResult(message, { retainDone: false, fallbackTimestamp: toRecord(entry).timestamp, }); } } this.initialized = true; this.markUnsettledInterrupted(); for (const group of this.groups) { this.recomputeLeader(group.groupId); group.settled = group.members.length > 0 && !group.members.some((member) => member.state === "pending" || member.state === "running"); } const staleInvalidators: Array<() => void> = []; for (const [toolCallId, invalidate] of this.invalidators) { if (this.membersById.get(toolCallId)?.visible !== true) { this.invalidators.delete(toolCallId); staleInvalidators.push(invalidate); } } for (const invalidate of staleInvalidators) { try { invalidate(); } catch { // A removed row may already belong to a disposed transcript. } } this.invalidateAll(); } getView(toolCallId: string): AggregateActivityView | undefined { const member = this.membersById.get(toolCallId); if (!member || member.state === "needsAttention" || this.isPassthrough(member.toolName)) return undefined; const group = this.groupsById.get(member.groupId); if (!group || group.leaderToolCallId !== toolCallId) return undefined; const grouped = group.members; const aggregateMembers = grouped.filter( (entry) => entry.state !== "needsAttention" && !this.isPassthrough(entry.toolName), ); const activeAll = aggregateMembers .filter((entry) => entry.state === "pending" || entry.state === "running") .sort((left, right) => left.sourceOrder - right.sourceOrder); const active = activeAll.slice(0, ACTIVE_ROW_LIMIT); const retainedDone = aggregateMembers .filter((entry) => entry.state === "success" && entry.retainedDone) .sort((left, right) => (right.completionOrder ?? 0) - (left.completionOrder ?? 0)) .slice(0, Math.max(0, ACTIVE_ROW_LIMIT - active.length)); const displayRows = [...active, ...retainedDone] .sort((left, right) => left.sourceOrder - right.sourceOrder); const failed = aggregateMembers .filter((entry) => entry.state === "failed") .sort((left, right) => left.sourceOrder - right.sourceOrder); const summaries = new Map(); for (const entry of [...grouped].sort((left, right) => left.sourceOrder - right.sourceOrder)) { const summary = summaries.get(entry.toolName); if (summary) { summary.count += 1; summary.lastTarget = formatAggregateTarget(entry); } else { summaries.set(entry.toolName, { toolName: entry.toolName, count: 1, lastTarget: formatAggregateTarget(entry), }); } } return { groupId: group.groupId, leaderToolCallId: toolCallId, hasRunning: grouped.some((entry) => entry.state === "pending" || entry.state === "running"), latestNarration: this.latestNarrationFor(toolCallId), callCount: grouped.length, agentTurnCount: Math.max(1, group.agentTurnIds.length), settled: group.settled, durationMs: group.startedAtMs !== undefined && group.endedAtMs !== undefined ? Math.max(0, group.endedAtMs - group.startedAtMs) : undefined, completedAtMs: group.endedAtMs, usage: sumUsage(group.usageByKey), active, displayRows, activeOverflow: Math.max(0, activeAll.length - ACTIVE_ROW_LIMIT), failed, failedCount: grouped.filter((entry) => entry.state === "failed").length, steerCount: group.steers.length, pinnedSteers: group.settled ? [] : group.steers.map((steer) => ({ id: steer.id, firstLine: steer.firstLine })), toolSummaries: [...summaries.values()], }; } private markGroupSawToolBatch(groupId = this.activeGroupId): void { if (!groupId) return; const group = this.groupsById.get(groupId); if (group) group.hasSeenToolBatch = true; } private maybeSettleFromTerminalAssistant(message: unknown): void { if (!isAssistantTerminal(message)) return; const group = this.activeGroupId ? this.groupsById.get(this.activeGroupId) : undefined; if (!group) return; if (group.members.some((member) => member.state === "pending" || member.state === "running")) { return; } group.settled = true; } private ensureGroup(groupId: string): AggregateGroup { let group = this.groupsById.get(groupId); if (!group) { group = { groupId, members: [], framedItemIds: [], narrationById: new Map(), agentTurnIds: [], usageByKey: new Map(), steers: [], hasSeenToolBatch: false, settled: false, }; this.groups.push(group); this.groupsById.set(groupId, group); } return group; } private ensureActiveGroup(): AggregateGroup { if (!this.activeGroupId) this.activeGroupId = `orphan-${++this.liveGroupSequence}`; return this.ensureGroup(this.activeGroupId); } private addOrUpdateMember( toolCallId: string, toolName: string, args: unknown, visible: boolean, ): AggregateMember { const existing = this.membersById.get(toolCallId); if (existing) { existing.args = { ...existing.args, ...toRecord(args) }; existing.toolName = toolName; const becameVisible = !existing.visible && visible; existing.visible ||= visible; if (becameVisible) this.recomputeLeader(existing.groupId); return existing; } const group = this.ensureActiveGroup(); group.hasSeenToolBatch = true; this.evictOldestRetainedDone(group); const previousLeader = group.leaderToolCallId; const member: AggregateMember = { toolCallId, toolName, groupId: group.groupId, sourceOrder: this.sourceOrder++, args: { ...toRecord(args) }, state: "pending", visible, }; group.members.push(member); this.membersById.set(toolCallId, member); if (visible && !this.isPassthrough(toolName)) { this.trackFramedItem(toolCallId, group.groupId); group.leaderToolCallId = toolCallId; } this.invalidateIds(previousLeader, group.leaderToolCallId); return member; } private evictOldestRetainedDone(group: AggregateGroup): void { const oldest = group.members .filter((member) => member.retainedDone) .sort((left, right) => (left.completionOrder ?? Number.MAX_SAFE_INTEGER) - (right.completionOrder ?? Number.MAX_SAFE_INTEGER), )[0]; if (!oldest) return; oldest.retainedDone = false; oldest.completionOrder = undefined; } private trimRetainedDone(groupId: string): void { const group = this.groupsById.get(groupId); if (!group) return; while (group.members.filter((member) => member.retainedDone).length > ACTIVE_ROW_LIMIT) { this.evictOldestRetainedDone(group); } } private recomputeLeader(groupId: string): void { const group = this.groupsById.get(groupId); if (!group) return; const previousLeader = group.leaderToolCallId; group.leaderToolCallId = [...group.members] .reverse() .find((member) => member.visible && member.state !== "needsAttention" && !this.isPassthrough(member.toolName), )?.toolCallId; this.invalidateIds(previousLeader, group.leaderToolCallId); } private invalidateGroup(groupId: string, changedId?: string): void { const group = this.groupsById.get(groupId); this.invalidateIds(group?.leaderToolCallId, changedId); } private invalidateIds(...ids: Array): void { const requested = new Set(ids.filter((entry): entry is string => Boolean(entry))); if (requested.size === 0) return; for (const id of requested) { try { this.invalidators.get(id)?.(); } catch { // Rendering must remain fail-open if a stale component rejects invalidation. } } for (const entry of this.frameInvalidators) { if (!requested.has(entry.id)) continue; try { entry.invalidate(); } catch { // A stale transcript component may already be disposed. } } } private invalidateAll(): void { for (const invalidate of this.invalidators.values()) { try { invalidate(); } catch { // Ignore stale render contexts after session replacement. } } } } function memberStatusChrome( member: Pick, theme: AggregateRenderTheme, ): { marker: string; suffix: string } { if (member.state === "failed") { const detail = member.errorSummary ?? "Tool failed."; return { marker: theme.fg("error", "!"), suffix: theme.fg("error", `: ${detail}`), }; } if (member.state === "success") { return { marker: theme.fg("success", "✓"), suffix: "", }; } return { marker: theme.fg("warning", "◐"), suffix: "", }; } export function framePrefixForEdge(edge: AggregateFrameEdge): string { return edge === "end" || edge === "only" ? AGGREGATE_FRAME_END : AGGREGATE_FRAME_CONTINUE; } export function applyAggregateGroupFrame( lines: readonly string[], width: number, theme: AggregateRenderTheme, edge: AggregateFrameEdge, ): string[] { const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0; if (safeWidth === 0 || lines.length === 0) return []; const lastIndex = lines.length - 1; return lines.map((line, index) => { const prefixPlain = index === lastIndex ? framePrefixForEdge(edge) : AGGREGATE_FRAME_CONTINUE; const prefix = theme.fg("muted", prefixPlain); const contentWidth = Math.max(0, safeWidth - visibleWidth(prefixPlain)); return `${prefix}${truncateToWidth(line, contentWidth, "…")}`; }); } export function padAggregateBlock(lines: readonly string[]): string[] { return lines.length > 0 ? ["", ...lines, ""] : []; } export function attachExpandedAggregateSummary( header: readonly string[], detail: readonly string[], ): string[] { if (header.length === 0) return [...detail]; if (detail.length === 0) return padAggregateBlock(header); return ["", ...header, ...detail]; } export function renderAggregateMemberRow( member: Pick, width: number, theme: AggregateRenderTheme, edge: AggregateFrameEdge = "only", ): string[] { const { marker, suffix } = memberStatusChrome(member, theme); return applyAggregateGroupFrame( [`${marker} ${formatColoredTarget(member, theme)}${suffix}`], width, theme, edge, ); } export function renderAggregateActivity( view: AggregateActivityView, width: number, theme: AggregateRenderTheme, ): string[] { const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0; if (safeWidth === 0) return []; const hasFailure = view.failedCount > 0; const marker = hasFailure ? "!" : view.hasRunning ? "◐" : "✓"; const markerColor = hasFailure ? "error" : view.hasRunning ? "warning" : "success"; const totals = theme.fg( "muted", ` (${view.callCount} call${view.callCount === 1 ? "" : "s"} · ${view.agentTurnCount} turn${view.agentTurnCount === 1 ? "" : "s"})`, ); let header = `${theme.fg(markerColor, marker)} ${theme.fg("toolTitle", theme.bold?.("Tools") ?? "Tools")}${totals}`; if (hasFailure) header += theme.fg("error", ` · ${view.failedCount} failed`); for (const summary of view.toolSummaries) { header += theme.fg("muted", " · "); header += theme.fg(toolColor(summary.toolName), `${summary.toolName} ×${summary.count}`); } const lines = [truncateToWidth(header, safeWidth, "…")]; if (view.settled) { lines.push(...renderSettledSteerReminder(view.steerCount ?? 0, safeWidth, theme)); } else { lines.push(...renderCollapsedSteerPins(view.pinnedSteers ?? [], safeWidth, theme)); } const stats = formatAggregateStatsLine(view); if (stats) { lines.push(truncateToWidth(` ${theme.fg("muted", stats)}`, safeWidth, "…")); } if (!view.settled && view.latestNarration) { lines.push(...renderCollapsedAssistantNarration(view.latestNarration, safeWidth, theme)); } for (const row of view.displayRows) { if (row.state === "success") { lines.push( truncateToWidth( ` ${theme.fg("success", "✓")} ${formatColoredTarget(row, theme)}`, safeWidth, "…", ), ); continue; } lines.push( truncateToWidth( ` ${theme.fg("warning", "◐")} ${formatColoredTarget(row, theme)}`, safeWidth, "…", ), ); } if (view.activeOverflow > 0) { lines.push( truncateToWidth(theme.fg("muted", ` … ${view.activeOverflow} more active`), safeWidth, "…"), ); } return lines; } function getToolExecutionPrototype(): PatchableToolExecutionPrototype { return ToolExecutionComponent.prototype as unknown as PatchableToolExecutionPrototype; } function createComponentInvalidator(component: PatchableToolExecution): () => void { return () => { try { component.invalidate?.(); component.ui?.requestRender?.(); } catch { // A stale transcript component may already be disposed. } }; } export function patchAggregateToolExecutions(projection: AggregateProjection): void { claimHostProjection(undefined, projection); const prototype = getToolExecutionPrototype(); const existing = prototype[AGGREGATE_TOOL_EXECUTION_PATCH_KEY]; if (existing) { if (prototype.render === existing.patchedRender || existing.projection !== undefined) { // A later session must not steal the already-painting host ledger. return; } // A wrapper installed before us may restore its own original render after // our cleanup, leaving only stale Symbol state. Start a fresh layer over // the actual current renderer; the disabled old closure remains harmless // if a surviving outer wrapper still references it. delete prototype[AGGREGATE_TOOL_EXECUTION_PATCH_KEY]; } const state = {} as AggregateToolExecutionPatchState; state.originalRender = prototype.render as AggregateToolExecutionPatchState["originalRender"]; state.projection = projection; state.patchedRender = function renderAggregateToolExecution(width: number): string[] { const toolName = normalizeToolName(this.toolName); const toolCallId = typeof this.toolCallId === "string" ? this.toolCallId : undefined; const activeProjection = resolveAggregateProjection(undefined, toolCallId) ?? state.projection; if (!activeProjection || !toolName || !toolCallId) { return state.originalRender.call(this, width); } activeProjection.connectRenderer( toolCallId, toolName, this.args, createComponentInvalidator(this), ); if (activeProjection.isPassthrough(toolName)) { return state.originalRender.call(this, width); } if (aggregateResultHasImage(this.result)) { activeProjection.markNeedsAttention(toolCallId); return state.originalRender.call(this, width); } if (!activeProjection.isInitialized()) return []; const member = activeProjection.getMember(toolCallId); if (member?.state === "needsAttention") return state.originalRender.call(this, width); if (!member) return []; const view = activeProjection.getView(toolCallId); if (this.expanded === true) { const detail = renderAggregateMemberRow( member, width, activeProjection.getRenderTheme(), activeProjection.getFrameEdge(toolCallId) ?? "only", ); if (activeProjection.shouldHostExpandedSummary(toolCallId)) { const headerView = activeProjection.getViewForGroup(toolCallId); if (headerView) { return attachExpandedAggregateSummary( renderAggregateActivity(headerView, width, activeProjection.getRenderTheme()), detail, ); } } return detail; } if (!view) return []; return padAggregateBlock(renderAggregateActivity(view, width, activeProjection.getRenderTheme())); }; Object.defineProperty(prototype, AGGREGATE_TOOL_EXECUTION_PATCH_KEY, { configurable: true, value: state, }); prototype.render = state.patchedRender; } export function restoreAggregateToolExecutions(): void { hostAggregateProjection = undefined; liveProjections.clear(); const prototype = getToolExecutionPrototype(); const state = prototype[AGGREGATE_TOOL_EXECUTION_PATCH_KEY]; if (!state) return; if (prototype.render === state.patchedRender) { prototype.render = state.originalRender; delete prototype[AGGREGATE_TOOL_EXECUTION_PATCH_KEY]; return; } state.projection = undefined; } function rebuildProjectionFromContext(projection: AggregateProjection, ctx: SessionContextLike): void { const sessionManager = ctx?.sessionManager; if (!sessionManager) return; let visibleMessages: unknown[] | undefined; try { visibleMessages = sessionManager.buildSessionContext?.().messages; } catch { visibleMessages = undefined; } projection.rebuild(sessionManager.getBranch(), visibleMessages); } export function registerAggregateProjectionEvents( pi: ExtensionAPI, projection: AggregateProjection, options: { doneSettleDelayMs?: number } = {}, ): void { const requestedDelay = options.doneSettleDelayMs ?? AGGREGATE_DONE_SETTLE_DELAY_MS; const doneSettleDelayMs = Number.isFinite(requestedDelay) ? Math.max(0, Math.floor(requestedDelay)) : AGGREGATE_DONE_SETTLE_DELAY_MS; let settleTimer: ReturnType | undefined; const clearSettleTimer = () => { if (settleTimer !== undefined) clearTimeout(settleTimer); settleTimer = undefined; }; const rebuild = (ctx: SessionContextLike) => { clearSettleTimer(); rebuildProjectionFromContext(projection, ctx); }; const adoptHostIfNeeded = () => { // A later session may keep its own ledger, but must not steal the host // prototype pointer or rebuild the already-painting host projection. if (claimHostProjection(pi, projection)) patchAggregateToolExecutions(projection); }; adoptHostIfNeeded(); onReloadShutdown(pi, () => { clearSettleTimer(); forgetProjection(pi, projection); // Only the last live ledger may drop the shared renderer patch. if (liveProjections.size === 0) restoreAggregateToolExecutions(); registeredApis.delete(pi); }); if (registeredApis.has(pi)) return; registeredApis.add(pi); pi.on("session_start", async (_event, ctx) => { if (ctx?.hasUI !== false) adoptHostIfNeeded(); rebuild(ctx); }); pi.on("before_agent_start", async (_event, ctx) => { if (ctx?.hasUI !== false) adoptHostIfNeeded(); rebuild(ctx); }); pi.on("session_compact", async (_event, ctx) => rebuild(ctx)); pi.on("session_tree", async (_event, ctx) => rebuild(ctx)); let pendingStreamingBehavior: "steer" | "followUp" | undefined; pi.on("input", async (event) => { pendingStreamingBehavior = event.streamingBehavior; }); pi.on("message_start", async (event) => { if (messageRole(event.message) === "user") { clearSettleTimer(); const behavior = pendingStreamingBehavior; pendingStreamingBehavior = undefined; projection.ingestUserMessage(event.message, { streamingBehavior: behavior }); } }); pi.on("message_update", async (event) => projection.ingestAssistantMessage(event.message)); pi.on("message_end", async (event) => { const role = messageRole(event.message); if (role === "assistant") projection.ingestAssistantMessage(event.message); else if (role === "toolResult") projection.ingestToolResult(event.message); }); pi.on("tool_execution_start", async (event) => { clearSettleTimer(); projection.markStarted(event.toolCallId, event.toolName, event.args); }); pi.on("tool_execution_update", async (event) => projection.markUpdated(event.toolCallId, event.args)); pi.on("tool_execution_end", async (event) => { projection.markComplete(event.toolCallId, event.result, event.isError === true); }); pi.on("agent_settled", async () => { projection.markUnsettledInterrupted(); projection.markGroupSettled(); clearSettleTimer(); settleTimer = setTimeout(() => { settleTimer = undefined; projection.collapseRetainedDone(); }, doneSettleDelayMs); settleTimer.unref?.(); }); }