import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent"; import { loadConfig } from "../config.ts"; import { getPresentationDesign } from "../tools/presentation/state.ts"; import { dropLeadingColumns, fgHex, startsWithVisibleSpace, stripAnsi } from "../theme/ansi.ts"; import { getThemeExtra } from "../theme/theme-extras.ts"; import { safeTakeTailToWidth, safeTruncateToWidth, safeVisibleWidth } from "../render-budget.ts"; import { attachComponentToStream } from "./assistant-streaming-state.ts"; import { type AssistantContentRun, getAssistantContentRuns, } from "./assistant-content-runs.ts"; let activeTheme: any = null; const PATCHED = Symbol.for("pi-ui.assistant-prefix.patched"); function buildPrefixSegment(): string { const prefix = getThemeExtra(activeTheme, "assistantPrefix"); const color = getThemeExtra(activeTheme, "assistantPrefixColor"); return activeTheme ? fgHex(activeTheme, color, prefix) : prefix; } function buildDividerLine(width: number): string { if (width <= 0) return ""; const char = getThemeExtra(activeTheme, "dividerChar"); const color = getThemeExtra(activeTheme, "dividerColor"); const line = char.repeat(width); return activeTheme ? fgHex(activeTheme, color, line) : line; } function composePrefixedLine(line: string): string { const prefix = buildPrefixSegment(); const design = getPresentationDesign(); if (design.compactLayout) { if (!line) return `${prefix}${design.markerGap}`; return startsWithVisibleSpace(line) ? `${prefix}${line}` : `${prefix}${design.markerGap}${line}`; } if (!line) return `${prefix} `; return startsWithVisibleSpace(line) ? `${prefix} ${line}` : `${prefix} ${line}`; } function compactReasonixLines(lines: string[]): string[] { let first = 0; while (first < lines.length && stripAnsi(lines[first] ?? "").trim() === "") first++; let last = lines.length - 1; while (last >= first && stripAnsi(lines[last] ?? "").trim() === "") last--; return first <= last ? [...lines.slice(first, last + 1), ""] : []; } function stripItalicAnsi(text: string): string { return text.replace(/\x1b\[3m/g, "").replace(/\x1b\[23m/g, ""); } function styleThinkingLine(text: string): string { if (!getPresentationDesign().compactLayout) return stripItalicAnsi(text); const plain = stripAnsi(text); if (plain.trim().length === 0 || typeof activeTheme?.fg !== "function") return plain; const colored = activeTheme.fg("thinkingText", plain); return typeof activeTheme.italic === "function" ? activeTheme.italic(colored) : colored; } function getAssistantBodyWidth(width: number): number { return Math.max(1, width - safeVisibleWidth(composePrefixedLine("")) - 1); } function addAssistantGutter(lines: string[]): string[] { const indent = " ".repeat(safeVisibleWidth(composePrefixedLine(""))); return lines.map((line) => { if (stripAnsi(line).trim().length === 0) return line; return `${indent}${dropLeadingColumns(line, 1)}`; }); } const COLLAPSED_TAIL_MIN_BUDGET = 16; const THINKING_TAIL_LIVE_MARKER = "▸"; const THINKING_TAIL_SETTLED_MARKER = "·"; const THINKING_BLOCK_MARKER_PATTERN = /^(?:#{1,6}\s+|[-*+]\s+|\d+\.\s+|>\s?)/; /** Last non-empty line of a thinking run, cleaned of a leading Markdown block marker and inner runs of whitespace. */ function cleanThinkingTailLine(text: string): string { return text.replace(/\s+/g, " ").trim().replace(THINKING_BLOCK_MARKER_PATTERN, "").trim(); } function lastThinkingTailLine(text: string): string { const lines = text.split("\n"); for (let i = lines.length - 1; i >= 0; i--) { const cleaned = cleanThinkingTailLine(lines[i] ?? ""); if (cleaned) return cleaned; } return ""; } /** The cut can land on whitespace; drop the space directly after the ellipsis (the row may end one column short). */ function trimTailWhitespaceAfterEllipsis(tail: string): string { if (!tail.startsWith("…")) return tail; return `…${tail.slice(1).replace(/^\s+/, "")}`; } /** The marker carries the progress state now, so a label's own trailing progress dots would contradict `·`. */ function trimLabelProgressDots(label: string): string { return label.replace(/[.…\s]+$/u, ""); } /** The collapsed label is a short upright tag: bold `thinkingText`, never italic, the same under every preset. */ function styleCollapsedLabel(label: string): string { if (typeof activeTheme?.fg !== "function") return label; const colored = activeTheme.fg("thinkingText", label); return typeof activeTheme.bold === "function" ? activeTheme.bold(colored) : `\x1b[1m${colored}\x1b[22m`; } /** One collapsed row: `