{"version":3,"file":"fleet-transcript.d.ts","sourceRoot":"","sources":["../../../src/tui/fleet-transcript.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAsC,MAAM,2BAA2B,CAAC;AACtG,OAAO,EAAY,KAAK,aAAa,EAAmD,MAAM,kBAAkB,CAAC;AA+FjH,KAAK,KAAK,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;AAE7C,MAAM,MAAM,oBAAoB,GAC7B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAClD;IACA,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,CAAC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CAClB,GACD;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7F,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,0BAA0B;IACnC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AAyRD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,0BAA0B,GAAG,eAAe,CA2B1G;AA+GD,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,eAAe,EAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,KAAK,EACZ,aAAa,EAAE,aAAa,EAC5B,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACvC,MAAM,EAAE,CA0EV","sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { type ExtensionContext, getLanguageFromPath, highlightCode } from \"@lpb-work/pi-coding-agent\";\nimport { Markdown, type MarkdownTheme, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@lpb-work/pi-tui\";\n\nconst DEFAULT_MAX_RECORDS = 240;\nconst DEFAULT_MAX_BYTES = 2 * 1024 * 1024;\nconst MAX_MESSAGE_CHARS = 64 * 1024;\nconst TOOL_PREVIEW_LINES = 7;\nconst BINARY_CONTENT_PLACEHOLDER = \"[binary content omitted for safe display]\";\n\nfunction isUnsafeDisplayCodePoint(codePoint: number): boolean {\n\tconst terminalControl =\n\t\t(codePoint <= 0x1f && codePoint !== 0x09 && codePoint !== 0x0a) || (codePoint >= 0x7f && codePoint <= 0x9f);\n\tconst bidiControl =\n\t\tcodePoint === 0x061c ||\n\t\tcodePoint === 0x200e ||\n\t\tcodePoint === 0x200f ||\n\t\t(codePoint >= 0x202a && codePoint <= 0x202e) ||\n\t\t(codePoint >= 0x2066 && codePoint <= 0x2069);\n\tconst privateUse =\n\t\t(codePoint >= 0xe000 && codePoint <= 0xf8ff) ||\n\t\t(codePoint >= 0xf0000 && codePoint <= 0xffffd) ||\n\t\t(codePoint >= 0x100000 && codePoint <= 0x10fffd);\n\tconst invalidScalar = codePoint >= 0xd800 && codePoint <= 0xdfff;\n\tconst nonCharacter =\n\t\t(codePoint >= 0xfdd0 && codePoint <= 0xfdef) ||\n\t\t(codePoint & 0xffff) === 0xfffe ||\n\t\t(codePoint & 0xffff) === 0xffff;\n\treturn terminalControl || bidiControl || privateUse || invalidScalar || nonCharacter;\n}\n\nfunction looksLikeBinaryContent(text: string): boolean {\n\tif (text.includes(\"\\0\")) return true;\n\tlet suspiciousControls = 0;\n\tlet replacementCharacters = 0;\n\tlet codePoints = 0;\n\tfor (const character of text) {\n\t\tcodePoints++;\n\t\tconst codePoint = character.codePointAt(0) ?? 0;\n\t\tif (codePoint <= 0x08 || (codePoint >= 0x0e && codePoint <= 0x1f)) suspiciousControls++;\n\t\tif (codePoint === 0xfffd) replacementCharacters++;\n\t}\n\tif (codePoints === 0) return false;\n\treturn (\n\t\t(suspiciousControls >= 4 && suspiciousControls / codePoints >= 0.1) ||\n\t\t(replacementCharacters >= 3 && replacementCharacters / codePoints >= 0.1)\n\t);\n}\n\nfunction safeDisplayText(text: string): string {\n\tconst normalized = text.replace(/\\r\\n/g, \"\\n\");\n\tif (looksLikeBinaryContent(normalized)) return BINARY_CONTENT_PLACEHOLDER;\n\tlet safe = \"\";\n\tfor (const character of normalized) {\n\t\tconst codePoint = character.codePointAt(0) ?? 0;\n\t\tsafe += isUnsafeDisplayCodePoint(codePoint)\n\t\t\t? `[U+${codePoint.toString(16).toUpperCase().padStart(4, \"0\")}]`\n\t\t\t: character;\n\t}\n\treturn safe;\n}\n\nfunction sanitizeJsonDisplayValue(value: unknown): { value: unknown; changed: boolean } {\n\tif (typeof value === \"string\") {\n\t\tconst safe = safeDisplayText(value);\n\t\treturn { value: safe, changed: safe !== value };\n\t}\n\tif (Array.isArray(value)) {\n\t\tconst sanitized = value.map(sanitizeJsonDisplayValue);\n\t\treturn {\n\t\t\tvalue: sanitized.map((entry) => entry.value),\n\t\t\tchanged: sanitized.some((entry) => entry.changed),\n\t\t};\n\t}\n\tif (value && typeof value === \"object\") {\n\t\tlet changed = false;\n\t\tconst sanitized: Record<string, unknown> = Object.create(null);\n\t\tfor (const [key, nested] of Object.entries(value)) {\n\t\t\tconst safeKey = safeDisplayText(key);\n\t\t\tconst safeValue = sanitizeJsonDisplayValue(nested);\n\t\t\tsanitized[safeKey] = safeValue.value;\n\t\t\tchanged ||= safeKey !== key || safeValue.changed;\n\t\t}\n\t\treturn { value: changed ? sanitized : value, changed };\n\t}\n\treturn { value, changed: false };\n}\n\nfunction safeToolArgsPayload(payload: string): string {\n\ttry {\n\t\tconst sanitized = sanitizeJsonDisplayValue(JSON.parse(payload));\n\t\treturn sanitized.changed ? JSON.stringify(sanitized.value) : safeDisplayText(payload);\n\t} catch {\n\t\treturn safeDisplayText(payload);\n\t}\n}\n\ntype Theme = ExtensionContext[\"ui\"][\"theme\"];\n\nexport type FleetTranscriptEvent =\n\t| { kind: \"assistant\"; text: string; model?: string; timestamp?: number }\n\t| { kind: \"user\"; text: string; timestamp?: number }\n\t| {\n\t\t\tkind: \"tool\";\n\t\t\ttoolCallId?: string;\n\t\t\tname: string;\n\t\t\targs?: string;\n\t\t\targsPayload?: string;\n\t\t\toutput?: string;\n\t\t\toutputTruncated?: boolean;\n\t\t\tstatus: \"running\" | \"complete\" | \"error\";\n\t\t\terror?: string;\n\t\t\tstartedAt?: number;\n\t\t\tendedAt?: number;\n\t\t\ttimestamp?: number;\n\t  }\n\t| { kind: \"notice\"; text: string; tone: \"muted\" | \"warning\" | \"error\"; timestamp?: number };\n\nexport interface FleetTranscript {\n\tpath: string;\n\tevents: FleetTranscriptEvent[];\n\ttruncated: boolean;\n\twarning?: string;\n}\n\ninterface FleetTranscriptReadOptions {\n\ttrustedRoots: string[];\n\tmaxRecords?: number;\n\tmaxBytes?: number;\n}\n\ninterface MutableToolEvent {\n\tkind: \"tool\";\n\ttoolCallId?: string;\n\tname: string;\n\targs?: string;\n\targsPayload?: string;\n\toutput?: string;\n\toutputTruncated?: boolean;\n\tstatus: \"running\" | \"complete\" | \"error\";\n\terror?: string;\n\tstartedAt?: number;\n\tendedAt?: number;\n\ttimestamp?: number;\n\tresultSeen?: boolean;\n}\n\nfunction objectValue(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction stringValue(value: unknown): string | undefined {\n\treturn typeof value === \"string\" && value.trim() ? value : undefined;\n}\n\nfunction numberValue(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\nfunction pathWithin(base: string, candidate: string): boolean {\n\tconst resolvedBase = path.resolve(base);\n\tconst resolvedCandidate = path.resolve(candidate);\n\treturn resolvedCandidate === resolvedBase || resolvedCandidate.startsWith(`${resolvedBase}${path.sep}`);\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction isNotFoundError(error: unknown): boolean {\n\treturn Boolean(objectValue(error)?.code === \"ENOENT\");\n}\n\nfunction validateTranscriptPath(filePath: string, trustedRoots: string[]): { resolvedPath?: string; warning?: string } {\n\tif (trustedRoots.length === 0) return { warning: `Transcript preview has no trusted root: ${filePath}` };\n\tconst resolvedPath = path.resolve(filePath);\n\tif (!trustedRoots.some((root) => pathWithin(root, resolvedPath))) {\n\t\treturn { warning: `Transcript is outside trusted roots: ${filePath}` };\n\t}\n\tlet stat: fs.Stats;\n\ttry {\n\t\tstat = fs.lstatSync(resolvedPath);\n\t} catch (error) {\n\t\tif (isNotFoundError(error)) return {};\n\t\treturn { warning: `Transcript could not be inspected: ${errorMessage(error)}` };\n\t}\n\tif (stat.isSymbolicLink()) return { warning: `Transcript preview refused a symlink: ${filePath}` };\n\tif (!stat.isFile()) return { warning: `Transcript path is not a file: ${filePath}` };\n\ttry {\n\t\tconst realPath = fs.realpathSync(resolvedPath);\n\t\tconst realRoots = trustedRoots.filter((root) => fs.existsSync(root)).map((root) => fs.realpathSync(root));\n\t\tif (!realRoots.some((root) => pathWithin(root, realPath))) {\n\t\t\treturn { warning: `Transcript resolves outside trusted roots: ${filePath}` };\n\t\t}\n\t\treturn { resolvedPath: realPath };\n\t} catch (error) {\n\t\treturn { warning: `Transcript path could not be resolved: ${errorMessage(error)}` };\n\t}\n}\n\nfunction isCompleteRecord(line: string | undefined): boolean {\n\tif (!line?.trim()) return false;\n\ttry {\n\t\treturn objectValue(JSON.parse(line)) !== undefined;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction readTailLines(filePath: string, maxBytes: number): { lines: string[]; truncated: boolean; warning?: string } {\n\tlet fd: number | undefined;\n\ttry {\n\t\tconst noFollow = typeof fs.constants.O_NOFOLLOW === \"number\" ? fs.constants.O_NOFOLLOW : 0;\n\t\tfd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);\n\t\tconst stat = fs.fstatSync(fd);\n\t\tif (!stat.isFile()) return { lines: [], truncated: false, warning: `Transcript path is not a file: ${filePath}` };\n\t\tif (stat.size === 0) return { lines: [], truncated: false };\n\t\tconst bytesToRead = Math.min(stat.size, maxBytes);\n\t\tconst start = stat.size - bytesToRead;\n\t\tconst buffer = Buffer.alloc(bytesToRead);\n\t\tconst bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, start);\n\t\tconst content = buffer.subarray(0, bytesRead).toString(\"utf-8\");\n\t\tconst endsWithNewline = content.endsWith(\"\\n\");\n\t\tlet lines = content.split(/\\r?\\n/);\n\t\tif (start > 0 && lines.length > 0) lines = lines.slice(1);\n\t\tif (lines.at(-1) === \"\") lines = lines.slice(0, -1);\n\t\telse if (!endsWithNewline && !isCompleteRecord(lines.at(-1))) lines = lines.slice(0, -1);\n\t\treturn { lines, truncated: start > 0 };\n\t} catch (error) {\n\t\treturn { lines: [], truncated: false, warning: `Transcript could not be read: ${errorMessage(error)}` };\n\t} finally {\n\t\tif (fd !== undefined) fs.closeSync(fd);\n\t}\n}\n\nfunction clipMessage(text: string): string {\n\tif (text.length <= MAX_MESSAGE_CHARS) return text;\n\treturn `${text.slice(0, MAX_MESSAGE_CHARS)}\\n\\n… message truncated`;\n}\n\nfunction safeTranscriptEvent(event: FleetTranscriptEvent): FleetTranscriptEvent {\n\tif (event.kind === \"assistant\") {\n\t\treturn {\n\t\t\t...event,\n\t\t\ttext: safeDisplayText(event.text),\n\t\t\t...(event.model ? { model: safeDisplayText(event.model) } : {}),\n\t\t};\n\t}\n\tif (event.kind === \"user\" || event.kind === \"notice\") {\n\t\treturn { ...event, text: safeDisplayText(event.text) };\n\t}\n\treturn {\n\t\t...event,\n\t\tname: safeDisplayText(event.name),\n\t\t...(event.args !== undefined ? { args: safeDisplayText(event.args) } : {}),\n\t\t...(event.argsPayload !== undefined ? { argsPayload: safeToolArgsPayload(event.argsPayload) } : {}),\n\t\t...(event.output !== undefined ? { output: safeDisplayText(event.output) } : {}),\n\t\t...(event.error !== undefined ? { error: safeDisplayText(event.error) } : {}),\n\t};\n}\n\nfunction findTool(\n\tevents: FleetTranscriptEvent[],\n\ttoolCallId: string | undefined,\n\tname: string | undefined,\n): MutableToolEvent | undefined {\n\tif (toolCallId) {\n\t\tfor (let index = events.length - 1; index >= 0; index--) {\n\t\t\tconst event = events[index];\n\t\t\tif (event?.kind === \"tool\" && event.toolCallId === toolCallId) return event as MutableToolEvent;\n\t\t}\n\t\treturn undefined;\n\t}\n\tfor (let index = events.length - 1; index >= 0; index--) {\n\t\tconst event = events[index];\n\t\tif (event?.kind !== \"tool\") continue;\n\t\tconst tool = event as MutableToolEvent;\n\t\tif ((!name || tool.name === name) && !tool.resultSeen) return tool;\n\t}\n\treturn undefined;\n}\n\nfunction appendTextEvent(\n\tevents: FleetTranscriptEvent[],\n\tkind: \"assistant\" | \"user\",\n\ttext: string,\n\tmetadata: { model?: string; timestamp?: number },\n): void {\n\tconst clipped = clipMessage(text.trim());\n\tif (!clipped) return;\n\tconst previous = events.at(-1);\n\tif (previous?.kind === kind && previous.text === clipped) return;\n\tevents.push({ kind, text: clipped, ...metadata });\n}\n\nfunction parseTranscriptLines(\n\tlines: string[],\n\tconversationStarted = false,\n): { events: FleetTranscriptEvent[]; malformed: number; explicitTruncation: boolean } {\n\tconst events: FleetTranscriptEvent[] = [];\n\tlet malformed = 0;\n\tlet explicitTruncation = false;\n\tlet assistantSeen = conversationStarted;\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\tlet record: Record<string, unknown> | undefined;\n\t\ttry {\n\t\t\trecord = objectValue(JSON.parse(line));\n\t\t} catch {\n\t\t\tmalformed++;\n\t\t\tcontinue;\n\t\t}\n\t\tif (!record) {\n\t\t\tmalformed++;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst recordType = stringValue(record.recordType);\n\t\tconst timestamp = numberValue(record.ts);\n\t\tif (recordType === \"truncated\") {\n\t\t\texplicitTruncation = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (recordType === \"tool_start\") {\n\t\t\tconst name = stringValue(record.toolName) ?? \"tool\";\n\t\t\tevents.push({\n\t\t\t\tkind: \"tool\",\n\t\t\t\t...(stringValue(record.toolCallId) ? { toolCallId: stringValue(record.toolCallId) } : {}),\n\t\t\t\tname,\n\t\t\t\t...(stringValue(record.argsPreview) ? { args: stringValue(record.argsPreview) } : {}),\n\t\t\t\t...(stringValue(record.argsPayload) ? { argsPayload: stringValue(record.argsPayload) } : {}),\n\t\t\t\tstatus: \"running\",\n\t\t\t\t...(timestamp !== undefined ? { timestamp, startedAt: timestamp } : {}),\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (recordType === \"tool_end\") {\n\t\t\tconst tool = findTool(events, stringValue(record.toolCallId), stringValue(record.toolName));\n\t\t\tif (tool && !tool.resultSeen) tool.status = record.isError === true ? \"error\" : \"complete\";\n\t\t\tif (tool && timestamp !== undefined && tool.endedAt === undefined) tool.endedAt = timestamp;\n\t\t\tcontinue;\n\t\t}\n\t\tif (recordType === \"stderr\") {\n\t\t\tconst text = stringValue(record.text);\n\t\t\tif (text)\n\t\t\t\tevents.push({\n\t\t\t\t\tkind: \"notice\",\n\t\t\t\t\ttext: clipMessage(text),\n\t\t\t\t\ttone: \"error\",\n\t\t\t\t\t...(timestamp !== undefined ? { timestamp } : {}),\n\t\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (recordType !== \"message\") continue;\n\n\t\tconst message = objectValue(record.message);\n\t\tconst role = stringValue(record.role) ?? stringValue(message?.role);\n\t\tconst text = stringValue(record.text) ?? stringValue(message?.text) ?? stringValue(message?.content);\n\t\tif (role === \"toolResult\" || role === \"tool_result\") {\n\t\t\tconst toolCallId = stringValue(record.toolCallId) ?? stringValue(message?.toolCallId);\n\t\t\tconst name = stringValue(record.toolName) ?? stringValue(message?.toolName) ?? \"tool\";\n\t\t\tconst failed = record.isError === true || message?.isError === true;\n\t\t\tlet tool = findTool(events, toolCallId, name);\n\t\t\tif (!tool) {\n\t\t\t\ttool = {\n\t\t\t\t\tkind: \"tool\",\n\t\t\t\t\t...(toolCallId ? { toolCallId } : {}),\n\t\t\t\t\tname,\n\t\t\t\t\tstatus: failed ? \"error\" : \"complete\",\n\t\t\t\t\t...(timestamp !== undefined ? { timestamp } : {}),\n\t\t\t\t};\n\t\t\t\tevents.push(tool);\n\t\t\t}\n\t\t\tif (!tool.resultSeen) {\n\t\t\t\ttool.resultSeen = true;\n\t\t\t\ttool.status = failed ? \"error\" : \"complete\";\n\t\t\t\tif (timestamp !== undefined && tool.endedAt === undefined) tool.endedAt = timestamp;\n\t\t\t\tif (text && (!failed || tool.output === undefined)) {\n\t\t\t\t\ttool.output = clipMessage(text);\n\t\t\t\t\ttool.outputTruncated =\n\t\t\t\t\t\trecord.outputTruncated === true ||\n\t\t\t\t\t\ttext.includes(\"… payload truncated\") ||\n\t\t\t\t\t\ttext.includes(\"[Showing lines\");\n\t\t\t\t}\n\t\t\t\tif (failed && text)\n\t\t\t\t\ttool.error = clipMessage(text.split(/\\r?\\n/).find((candidate) => candidate.trim()) ?? text);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (role === \"assistant\") {\n\t\t\tassistantSeen = true;\n\t\t\tif (text)\n\t\t\t\tappendTextEvent(events, \"assistant\", text, {\n\t\t\t\t\t...(stringValue(record.model) ? { model: stringValue(record.model) } : {}),\n\t\t\t\t\t...(timestamp !== undefined ? { timestamp } : {}),\n\t\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\t\tif (role === \"user\" && assistantSeen && text) {\n\t\t\tappendTextEvent(events, \"user\", text, timestamp !== undefined ? { timestamp } : {});\n\t\t}\n\t}\n\n\tfor (const event of events) {\n\t\tif (event.kind === \"tool\") delete (event as MutableToolEvent).resultSeen;\n\t}\n\treturn { events: events.map(safeTranscriptEvent), malformed, explicitTruncation };\n}\n\nexport function readFleetTranscript(filePath: string, options: FleetTranscriptReadOptions): FleetTranscript {\n\tconst validated = validateTranscriptPath(filePath, options.trustedRoots);\n\tif (!validated.resolvedPath) {\n\t\treturn {\n\t\t\tpath: filePath,\n\t\t\tevents: [],\n\t\t\ttruncated: false,\n\t\t\t...(validated.warning ? { warning: safeDisplayText(validated.warning) } : {}),\n\t\t};\n\t}\n\tconst maxRecords = Math.max(1, options.maxRecords ?? DEFAULT_MAX_RECORDS);\n\tconst tail = readTailLines(validated.resolvedPath, Math.max(1024, options.maxBytes ?? DEFAULT_MAX_BYTES));\n\tconst recordsOmitted = tail.truncated || tail.lines.length > maxRecords;\n\tconst selectedLines = tail.lines.slice(-maxRecords);\n\tconst parsed = parseTranscriptLines(selectedLines, recordsOmitted);\n\tconst warnings = [\n\t\ttail.warning,\n\t\tparsed.malformed > 0\n\t\t\t? `Skipped ${parsed.malformed} malformed transcript record${parsed.malformed === 1 ? \"\" : \"s\"}.`\n\t\t\t: undefined,\n\t].filter((value): value is string => Boolean(value));\n\treturn {\n\t\tpath: filePath,\n\t\tevents: parsed.events,\n\t\ttruncated: tail.truncated || tail.lines.length > maxRecords || parsed.explicitTruncation,\n\t\t...(warnings.length ? { warning: safeDisplayText(warnings.join(\" \")) } : {}),\n\t};\n}\n\nfunction statusGlyph(event: Extract<FleetTranscriptEvent, { kind: \"tool\" }>, theme: Theme): string {\n\tif (event.status === \"running\") return theme.fg(\"warning\", \"●\");\n\tif (event.status === \"error\") return theme.fg(\"error\", \"✗\");\n\treturn theme.fg(\"success\", \"✓\");\n}\n\nfunction jsonScalar(value: unknown): string | undefined {\n\tif (typeof value === \"string\" && value.trim()) return value;\n\tif (typeof value === \"number\" || typeof value === \"boolean\") return String(value);\n\treturn undefined;\n}\n\nfunction parseToolArgs(event: Extract<FleetTranscriptEvent, { kind: \"tool\" }>): Record<string, unknown> | undefined {\n\tif (!event.argsPayload) return undefined;\n\ttry {\n\t\treturn objectValue(JSON.parse(event.argsPayload));\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction toolDuration(event: Extract<FleetTranscriptEvent, { kind: \"tool\" }>): string | undefined {\n\tif (event.startedAt === undefined || event.endedAt === undefined) return undefined;\n\treturn `${((event.endedAt - event.startedAt) / 1000).toFixed(1)}s`;\n}\n\nfunction renderExpandedTool(\n\tevent: Extract<FleetTranscriptEvent, { kind: \"tool\" }>,\n\twidth: number,\n\ttheme: Theme,\n): string[] {\n\tconst lines: string[] = [];\n\tconst args = parseToolArgs(event);\n\tconst glyph = statusGlyph(event, theme);\n\tconst output = event.output ?? event.error;\n\tconst outputColor = event.status === \"error\" ? \"error\" : \"toolOutput\";\n\tif (event.name === \"bash\") {\n\t\tconst command = jsonScalar(args?.command) ?? event.args ?? \"(unknown command)\";\n\t\tlines.push(railLine(`${glyph} ${theme.fg(\"toolTitle\", theme.bold(`$ ${command}`))}`, width, theme));\n\t\tif (output) {\n\t\t\tfor (const outputLine of output.replace(/\\s+$/, \"\").split(/\\r?\\n/)) {\n\t\t\t\tfor (const wrapped of renderWrapped(theme.fg(outputColor, outputLine), Math.max(1, width - 4))) {\n\t\t\t\t\tlines.push(railLine(`  ${wrapped}`, width, theme));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tconst duration = toolDuration(event);\n\t\tif (duration) lines.push(railLine(theme.fg(\"dim\", `  Took ${duration}`), width, theme));\n\t\treturn lines;\n\t}\n\tif (event.name === \"read\") {\n\t\tconst filePath = jsonScalar(args?.path ?? args?.file_path);\n\t\tconst language = filePath ? getLanguageFromPath(filePath) : undefined;\n\t\tconst rendered = !output\n\t\t\t? []\n\t\t\t: event.status === \"error\"\n\t\t\t\t? output.split(\"\\n\").map((line) => theme.fg(\"error\", line))\n\t\t\t\t: language\n\t\t\t\t\t? highlightCode(output, language)\n\t\t\t\t\t: output.split(\"\\n\");\n\t\tlines.push(\n\t\t\trailLine(\n\t\t\t\t`${glyph} ${theme.fg(\"toolTitle\", theme.bold(`read ${filePath ?? event.args ?? \"\"}`))}`,\n\t\t\t\twidth,\n\t\t\t\ttheme,\n\t\t\t),\n\t\t);\n\t\tfor (const line of rendered) {\n\t\t\tfor (const wrapped of renderWrapped(line, Math.max(1, width - 4)))\n\t\t\t\tlines.push(railLine(`  ${wrapped}`, width, theme));\n\t\t}\n\t\treturn lines;\n\t}\n\tlines.push(railLine(`${glyph} ${theme.fg(\"toolTitle\", theme.bold(event.name))}`, width, theme));\n\tif (event.argsPayload) {\n\t\tlines.push(railLine(theme.fg(\"dim\", \"  args\"), width, theme));\n\t\tfor (const argLine of event.argsPayload.split(/\\r?\\n/)) {\n\t\t\tfor (const wrapped of renderWrapped(theme.fg(\"muted\", argLine), Math.max(1, width - 4)))\n\t\t\t\tlines.push(railLine(`  ${wrapped}`, width, theme));\n\t\t}\n\t}\n\tif (output) {\n\t\tlines.push(\n\t\t\trailLine(\n\t\t\t\ttheme.fg(event.status === \"error\" ? \"error\" : \"dim\", event.status === \"error\" ? \"  error\" : \"  output\"),\n\t\t\t\twidth,\n\t\t\t\ttheme,\n\t\t\t),\n\t\t);\n\t\tfor (const outputLine of output.split(/\\r?\\n/)) {\n\t\t\tfor (const wrapped of renderWrapped(theme.fg(outputColor, outputLine), Math.max(1, width - 4)))\n\t\t\t\tlines.push(railLine(`  ${wrapped}`, width, theme));\n\t\t}\n\t}\n\treturn lines;\n}\n\nfunction bounded(text: string, width: number): string {\n\treturn truncateToWidth(text, Math.max(0, width));\n}\n\nfunction railLine(content: string, width: number, theme: Theme): string {\n\treturn bounded(`${theme.fg(\"borderMuted\", \"│\")} ${content}`, width);\n}\n\nfunction renderWrapped(text: string, width: number): string[] {\n\treturn wrapTextWithAnsi(text, Math.max(1, width));\n}\n\nexport function renderFleetTranscript(\n\ttranscript: FleetTranscript,\n\twidth: number,\n\ttheme: Theme,\n\tmarkdownTheme: MarkdownTheme,\n\toptions: { expandedTools?: boolean } = {},\n): string[] {\n\tif (width <= 0) return [];\n\tconst lines: string[] = [];\n\tif (transcript.truncated) lines.push(bounded(theme.fg(\"dim\", \"↑ Earlier activity omitted\"), width));\n\tif (transcript.warning) {\n\t\tfor (const line of renderWrapped(safeDisplayText(transcript.warning), Math.max(1, width - 2))) {\n\t\t\tlines.push(bounded(`${theme.fg(\"warning\", \"!\")} ${theme.fg(\"warning\", line)}`, width));\n\t\t}\n\t}\n\n\tfor (const rawEvent of transcript.events) {\n\t\tconst event = safeTranscriptEvent(rawEvent);\n\t\tif (event.kind === \"tool\") {\n\t\t\tif (options.expandedTools && (event.output || event.argsPayload || event.error)) {\n\t\t\t\tlines.push(...renderExpandedTool(event, width, theme));\n\t\t\t\tlines.push(railLine(theme.fg(\"dim\", \"  x to collapse\"), width, theme));\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst title = theme.fg(\"toolTitle\", theme.bold(event.name));\n\t\t\tconst args = event.args ? ` ${theme.fg(\"dim\", event.args)}` : \"\";\n\t\t\tconst suffix = event.status === \"running\" ? theme.fg(\"warning\", \" running\") : \"\";\n\t\t\tlines.push(\n\t\t\t\tbounded(`${theme.fg(\"borderMuted\", \"├─\")} ${statusGlyph(event, theme)} ${title}${args}${suffix}`, width),\n\t\t\t);\n\t\t\tif (event.output && event.status !== \"error\" && event.name === \"bash\") {\n\t\t\t\tconst outputLines = event.output.replace(/\\s+$/, \"\").split(/\\r?\\n/);\n\t\t\t\tconst visible = outputLines.slice(-TOOL_PREVIEW_LINES);\n\t\t\t\tconst hidden = Math.max(0, outputLines.length - visible.length);\n\t\t\t\tfor (const outputLine of visible) {\n\t\t\t\t\tfor (const wrapped of renderWrapped(theme.fg(\"toolOutput\", outputLine), Math.max(1, width - 4))) {\n\t\t\t\t\t\tlines.push(railLine(`  ${wrapped}`, width, theme));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (hidden > 0)\n\t\t\t\t\tlines.push(railLine(theme.fg(\"dim\", `  … ${hidden} earlier lines · x to expand`), width, theme));\n\t\t\t\tconst duration = toolDuration(event);\n\t\t\t\tlines.push(railLine(theme.fg(\"dim\", `  Took${duration ? ` ${duration}` : \"\"}`), width, theme));\n\t\t\t} else if (event.output && event.status !== \"error\") {\n\t\t\t\tconst summary = truncateToWidth(event.output.replace(/\\s+/g, \" \").trim(), Math.max(1, width - 18), \"…\");\n\t\t\t\tif (summary) lines.push(railLine(theme.fg(\"dim\", `  ${summary} · x to expand`), width, theme));\n\t\t\t}\n\t\t\tif (event.error) {\n\t\t\t\tfor (const errorLine of renderWrapped(event.error, Math.max(1, width - 4))) {\n\t\t\t\t\tlines.push(railLine(theme.fg(\"error\", `  ${errorLine}`), width, theme));\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tif (event.kind === \"notice\") {\n\t\t\tconst color = event.tone === \"error\" ? \"error\" : event.tone === \"warning\" ? \"warning\" : \"dim\";\n\t\t\tfor (const noticeLine of renderWrapped(event.text, Math.max(1, width - 2))) {\n\t\t\t\tlines.push(railLine(theme.fg(color, noticeLine), width, theme));\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst assistant = event.kind === \"assistant\";\n\t\tconst label = assistant ? \"Assistant\" : \"Supervisor\";\n\t\tconst marker = assistant ? theme.fg(\"accent\", \"◆\") : theme.fg(\"warning\", \"◇\");\n\t\tconst model = assistant && event.model ? theme.fg(\"dim\", ` · ${event.model}`) : \"\";\n\t\tlines.push(bounded(`${marker} ${theme.bold(label)}${model}`, width));\n\t\tif (assistant) {\n\t\t\tconst rendered = new Markdown(event.text, 0, 0, markdownTheme).render(Math.max(1, width - 2));\n\t\t\tfor (const markdownLine of rendered) lines.push(railLine(markdownLine, width, theme));\n\t\t} else {\n\t\t\tfor (const userLine of renderWrapped(event.text, Math.max(1, width - 2))) {\n\t\t\t\tlines.push(railLine(userLine, width, theme));\n\t\t\t}\n\t\t}\n\t\tlines.push(theme.fg(\"borderMuted\", \"│\"));\n\t}\n\n\twhile (lines.length > 0 && visibleWidth(lines.at(-1) ?? \"\") === 1) lines.pop();\n\treturn lines;\n}\n"]}