// Shared preview bound: every core-tool preview is capped at 2000 chars so a // single tool call row can never flood the cell render. Lines that do not fit // are dropped head/tail (mirroring the pi-fabric previewEntries shape) with a // hidden-lines marker in between. export const PREVIEW_BOUND_CHARS = 2000; export interface BoundedPreview { readonly hidden: number; readonly lines: readonly string[]; } export function boundPreviewLines( lines: readonly string[], bound: number = PREVIEW_BOUND_CHARS ): BoundedPreview { const widths = lines.map(codePointLength); const total = widths.reduce((sum, width) => sum + width + 1, 0); if (total <= bound) { return { lines, hidden: 0 }; } const headBudget = Math.floor(bound * 0.65); const tailBudget = bound - headBudget; let headEnd = 0; let headChars = 0; while ( headEnd < lines.length && headChars + widths[headEnd] + 1 <= headBudget ) { headChars += widths[headEnd] + 1; headEnd += 1; } let tailStart = lines.length; let tailChars = 0; while ( tailStart > headEnd && tailChars + widths[tailStart - 1] + 1 <= tailBudget ) { tailChars += widths[tailStart - 1] + 1; tailStart -= 1; } const hidden = tailStart - headEnd; if (headEnd === 0 && tailStart === lines.length) { const first = lines[0] ?? ""; return { lines: [`${truncateCodePoints(first, bound)}…`], hidden: lines.length - 1, }; } const marker = `--- ${hidden} lines hidden ---`; return { lines: [...lines.slice(0, headEnd), marker, ...lines.slice(tailStart)], hidden, }; } function codePointLength(text: string): number { return Array.from(text).length; } function truncateCodePoints(text: string, max: number): string { return Array.from(text).slice(0, max).join(""); }