import type { TranscriptItem } from "./agentSessionTypes"; export type TranscriptTurnSegment = | { kind: "item"; item: TranscriptItem } | { kind: "setup"; items: Extract[] } | { kind: "prompt"; user: Extract; context: Extract | null; setup: Extract[]; }; export type TranscriptDisplayBlock = | { kind: "single"; item: TranscriptItem } | { kind: "turn"; turnId: string; segments: TranscriptTurnSegment[] }; function isUserPrompt( item: TranscriptItem, ): item is Extract { return ( item.type === "message" && item.role === "user" && item.acpSource === "session/prompt:user" ); } function isPromptContext( item: TranscriptItem, ): item is Extract { return ( item.type === "metadata" && item.acpSource === "session/prompt:context" ); } function isSetupLifecycle( item: TranscriptItem, ): item is Extract { return ( item.type === "lifecycle" && (item.acpSource === "turn_started" || item.acpSource === "session_resolved") ); } function isErrorLifecycle( item: TranscriptItem, ): item is Extract { return ( item.type === "lifecycle" && item.title.toLowerCase().includes("error") ); } type TurnBucket = { turnId: string; items: TranscriptItem[]; }; function classifyTurnItems(items: TranscriptItem[]): TranscriptTurnSegment[] { const userPrompt = items.find(isUserPrompt) ?? null; const setupLifecycle = items.filter(isSetupLifecycle); const promptContext = items.find(isPromptContext) ?? null; const consumed = new Set(); if (userPrompt) consumed.add(userPrompt); for (const item of setupLifecycle) consumed.add(item); if (promptContext) consumed.add(promptContext); const activity = items.filter((item) => !consumed.has(item)); if (!userPrompt) { return items.map((item) => ({ kind: "item", item })); } const segments: TranscriptTurnSegment[] = [ { kind: "prompt", user: userPrompt, context: promptContext, setup: setupLifecycle, }, ]; for (const item of activity) { if (isErrorLifecycle(item)) { segments.push({ kind: "item", item }); continue; } if (isSetupLifecycle(item)) { continue; } segments.push({ kind: "item", item }); } return segments; } /** * Build presentation-only display blocks from normalized transcript items. * Raw observer order is preserved in the source items; this only reorders * within a turn for user-facing narrative flow. */ export function buildTranscriptDisplayBlocks( items: TranscriptItem[], ): TranscriptDisplayBlock[] { const blocks: TranscriptDisplayBlock[] = []; const turnBuckets = new Map(); const displayOrder: Array< { kind: "single"; item: TranscriptItem } | { kind: "turn"; turnId: string } > = []; for (const item of items) { const turnId = item.turnId; if (!turnId) { displayOrder.push({ kind: "single", item }); continue; } let bucket = turnBuckets.get(turnId); if (!bucket) { bucket = { turnId, items: [] }; turnBuckets.set(turnId, bucket); displayOrder.push({ kind: "turn", turnId }); } bucket.items.push(item); } for (const entry of displayOrder) { if (entry.kind === "single") { blocks.push({ kind: "single", item: entry.item }); continue; } const bucket = turnBuckets.get(entry.turnId); if (!bucket || bucket.items.length === 0) { continue; } blocks.push({ kind: "turn", turnId: entry.turnId, segments: classifyTurnItems(bucket.items), }); } return blocks; } /** Flatten display blocks back to items for testing display order. */ export function flattenDisplayBlocks( blocks: TranscriptDisplayBlock[], ): TranscriptItem[] { const result: TranscriptItem[] = []; for (const block of blocks) { if (block.kind === "single") { result.push(block.item); continue; } for (const segment of block.segments) { if (segment.kind === "item") { result.push(segment.item); } else if (segment.kind === "prompt") { result.push(segment.user); result.push(...segment.setup); if (segment.context) { result.push(segment.context); } } else { result.push(...segment.items); } } } return result; } /** Human-readable labels for a collapsed turn setup row. */ export function formatTurnSetupLabel( items: Extract[], ): string { const labels = items.map((item) => item.title); return labels.join(" ยท "); } /** Earliest timestamp among setup lifecycle items. */ export function turnSetupTimestamp( items: Extract[], ): string | null { if (items.length === 0) return null; return items.reduce( (earliest, item) => Date.parse(item.timestamp) < Date.parse(earliest) ? item.timestamp : earliest, items[0].timestamp, ); } /** Optional detail text from setup lifecycle items (e.g. trigger count). */ export function turnSetupDetail( items: Extract[], ): string | null { const details = items .map((item) => item.text.trim()) .filter((text) => text.length > 0); if (details.length === 0) return null; return details.join(" "); }