|
| 1 | +export type ExecApprovalCommandPromptSummary = { |
| 2 | + text: string; |
| 3 | + truncated: boolean; |
| 4 | + totalLineCount: number; |
| 5 | + shownLineCount: number; |
| 6 | + hiddenLineCount: number; |
| 7 | + totalCharCount: number; |
| 8 | + hiddenCharCount: number; |
| 9 | +}; |
| 10 | + |
| 11 | +export const EXEC_APPROVAL_PROMPT_COMMAND_MAX_LINES = 5; |
| 12 | +export const EXEC_APPROVAL_PROMPT_COMMAND_MAX_CHARS = 1_200; |
| 13 | + |
| 14 | +const LOGICAL_LINE_BREAK_PATTERN = |
| 15 | + /\\u\{D\}\\u\{A\}|\\u\{A\}|\\u\{D\}|\\u\{2028\}|\\u\{2029\}|\r\n?|\n/gu; |
| 16 | + |
| 17 | +function positiveLimit(value: number | undefined, fallback: number): number { |
| 18 | + return Number.isSafeInteger(value) && typeof value === "number" && value > 0 ? value : fallback; |
| 19 | +} |
| 20 | + |
| 21 | +function plural(count: number, singular: string): string { |
| 22 | + return count === 1 ? singular : `${singular}s`; |
| 23 | +} |
| 24 | + |
| 25 | +function splitDisplayLines(text: string): string[] { |
| 26 | + const lines: string[] = []; |
| 27 | + let cursor = 0; |
| 28 | + for (const match of text.matchAll(LOGICAL_LINE_BREAK_PATTERN)) { |
| 29 | + const index = match.index; |
| 30 | + if (typeof index !== "number") { |
| 31 | + continue; |
| 32 | + } |
| 33 | + const marker = match[0]; |
| 34 | + const markerText = marker.startsWith("\\u{") ? marker : ""; |
| 35 | + lines.push(text.slice(cursor, index) + markerText); |
| 36 | + cursor = index + marker.length; |
| 37 | + } |
| 38 | + lines.push(text.slice(cursor)); |
| 39 | + return lines; |
| 40 | +} |
| 41 | + |
| 42 | +function buildTruncationMarker(params: { |
| 43 | + totalLineCount: number; |
| 44 | + shownLineCount: number; |
| 45 | + hiddenLineCount: number; |
| 46 | + hiddenCharCount: number; |
| 47 | +}): string { |
| 48 | + const details: string[] = []; |
| 49 | + if (params.hiddenLineCount > 0) { |
| 50 | + details.push(`showing first ${params.shownLineCount} of ${params.totalLineCount} lines`); |
| 51 | + } |
| 52 | + if (params.hiddenCharCount > 0) { |
| 53 | + details.push(`${params.hiddenCharCount} ${plural(params.hiddenCharCount, "char")} hidden`); |
| 54 | + } |
| 55 | + return `...[truncated: ${details.join("; ")}]`; |
| 56 | +} |
| 57 | + |
| 58 | +export function summarizeExecApprovalCommandForPrompt( |
| 59 | + commandText: string, |
| 60 | + options?: { maxLines?: number; maxChars?: number }, |
| 61 | +): ExecApprovalCommandPromptSummary { |
| 62 | + const maxLines = positiveLimit(options?.maxLines, EXEC_APPROVAL_PROMPT_COMMAND_MAX_LINES); |
| 63 | + const maxChars = positiveLimit(options?.maxChars, EXEC_APPROVAL_PROMPT_COMMAND_MAX_CHARS); |
| 64 | + const lines = splitDisplayLines(commandText); |
| 65 | + const shownLines = lines.slice(0, maxLines); |
| 66 | + const lineTruncated = lines.length > shownLines.length; |
| 67 | + if (!lineTruncated && commandText.length <= maxChars) { |
| 68 | + return { |
| 69 | + text: commandText, |
| 70 | + truncated: false, |
| 71 | + totalLineCount: lines.length, |
| 72 | + shownLineCount: lines.length, |
| 73 | + hiddenLineCount: 0, |
| 74 | + totalCharCount: commandText.length, |
| 75 | + hiddenCharCount: 0, |
| 76 | + }; |
| 77 | + } |
| 78 | + let text = shownLines.join("\n"); |
| 79 | + const totalText = lines.join("\n"); |
| 80 | + if (text.length > maxChars) { |
| 81 | + text = text.slice(0, maxChars).trimEnd(); |
| 82 | + } |
| 83 | + const hiddenLineCount = lineTruncated ? lines.length - shownLines.length : 0; |
| 84 | + const hiddenCharCount = Math.max(0, totalText.length - text.length); |
| 85 | + const truncated = hiddenLineCount > 0 || hiddenCharCount > 0; |
| 86 | + if (!truncated) { |
| 87 | + return { |
| 88 | + text, |
| 89 | + truncated: false, |
| 90 | + totalLineCount: lines.length, |
| 91 | + shownLineCount: lines.length, |
| 92 | + hiddenLineCount: 0, |
| 93 | + totalCharCount: totalText.length, |
| 94 | + hiddenCharCount: 0, |
| 95 | + }; |
| 96 | + } |
| 97 | + const marker = buildTruncationMarker({ |
| 98 | + totalLineCount: lines.length, |
| 99 | + shownLineCount: shownLines.length, |
| 100 | + hiddenLineCount, |
| 101 | + hiddenCharCount, |
| 102 | + }); |
| 103 | + return { |
| 104 | + text: text ? `${text}\n${marker}` : marker, |
| 105 | + truncated: true, |
| 106 | + totalLineCount: lines.length, |
| 107 | + shownLineCount: shownLines.length, |
| 108 | + hiddenLineCount, |
| 109 | + totalCharCount: totalText.length, |
| 110 | + hiddenCharCount, |
| 111 | + }; |
| 112 | +} |
0 commit comments