| 1 | const MAX_TERMINAL_CHAT_CHARS = 120_000; |
| 2 | |
| 3 | function consumeCSI(value: string, start: number): number { |
| 4 | let index = start; |
| 5 | while (index < value.length) { |
| 6 | const code = value.charCodeAt(index); |
| 7 | index += 1; |
| 8 | if (code >= 0x40 && code <= 0x7e) return index; |
| 9 | } |
| 10 | return value.length; |
| 11 | } |
| 12 | |
| 13 | function consumeStringSequence(value: string, start: number): number { |
| 14 | let index = start; |
| 15 | while (index < value.length) { |
| 16 | const code = value.charCodeAt(index); |
| 17 | if (code === 0x07 || code === 0x9c) return index + 1; |
| 18 | if (code === 0x1b && value.charCodeAt(index + 1) === 0x5c) return index + 2; |
| 19 | index += 1; |
| 20 | } |
| 21 | return value.length; |
| 22 | } |
| 23 | |
| 24 | function consumeEscapeSequence(value: string, start: number): number { |
| 25 | const first = value.charCodeAt(start + 1); |
| 26 | if (first === 0x5b) return consumeCSI(value, start + 2); |
| 27 | if (first === 0x5d || first === 0x50 || first === 0x5e || first === 0x5f || first === 0x58) { |
| 28 | return consumeStringSequence(value, start + 2); |
| 29 | } |
| 30 | let index = start + 1; |
| 31 | while (index < value.length) { |
| 32 | const code = value.charCodeAt(index); |
| 33 | index += 1; |
| 34 | if (code >= 0x30 && code <= 0x7e) return index; |
| 35 | if (code < 0x20 || code > 0x2f) return index; |
| 36 | } |
| 37 | return value.length; |
| 38 | } |
| 39 | |
| 40 | function consumeC1Sequence(value: string, start: number): number { |
| 41 | const code = value.charCodeAt(start); |
| 42 | if (code === 0x9b) { |
| 43 | return consumeCSI(value, start + 1); |
| 44 | } |
| 45 | if (code === 0x90 || code === 0x98 || code === 0x9d || code === 0x9e || code === 0x9f) { |
| 46 | return consumeStringSequence(value, start + 1); |
| 47 | } |
| 48 | return start + 1; |
| 49 | } |
| 50 | |
| 51 | export function stripTerminalControlSequences(value: string): string { |
| 52 | let result = ""; |
| 53 | for (let index = 0; index < value.length;) { |
| 54 | const code = value.charCodeAt(index); |
| 55 | if (code === 0x1b) { |
| 56 | index = consumeEscapeSequence(value, index); |
| 57 | continue; |
| 58 | } |
| 59 | if (code >= 0x80 && code <= 0x9f) { |
| 60 | index = consumeC1Sequence(value, index); |
| 61 | continue; |
| 62 | } |
| 63 | if (code < 0x20 && code !== 0x09 && code !== 0x0a && code !== 0x0d) { |
| 64 | index += 1; |
| 65 | continue; |
| 66 | } |
| 67 | if (code === 0x7f) { |
| 68 | index += 1; |
| 69 | continue; |
| 70 | } |
| 71 | result += value[index]; |
| 72 | index += 1; |
| 73 | } |
| 74 | return result.replace(/\r\n?/g, "\n"); |
| 75 | } |
| 76 | |
| 77 | function markdownFenceFor(value: string): string { |
| 78 | let longestRun = 0; |
| 79 | let currentRun = 0; |
| 80 | for (const character of value) { |
| 81 | if (character === "`") { |
| 82 | currentRun += 1; |
| 83 | longestRun = Math.max(longestRun, currentRun); |
| 84 | } else { |
| 85 | currentRun = 0; |
| 86 | } |
| 87 | } |
| 88 | return "`".repeat(Math.max(3, longestRun + 1)); |
| 89 | } |
| 90 | |
| 91 | export function formatTerminalOutputForComposer(value: string, maxChars = MAX_TERMINAL_CHAT_CHARS): string { |
| 92 | const normalized = stripTerminalControlSequences(value); |
| 93 | const clipped = normalized.length > maxChars ? normalized.slice(-maxChars) : normalized; |
| 94 | if (!clipped.trim()) return ""; |
| 95 | const fence = markdownFenceFor(clipped); |
| 96 | return `${fence}console\n${clipped}\n${fence}`; |
| 97 | } |
| 98 |