source dump of claude code
at main 131 lines 4.4 kB view raw
1import chalk from 'chalk' 2import { ctrlOToExpand } from '../components/CtrlOToExpand.js' 3import { stringWidth } from '../ink/stringWidth.js' 4import sliceAnsi from './sliceAnsi.js' 5 6// Text rendering utilities for terminal display 7const MAX_LINES_TO_SHOW = 3 8// Account for MessageResponse prefix (" ⎿ " = 5 chars) + parent width 9// reduction (columns - 5 in tool result rendering) 10const PADDING_TO_PREVENT_OVERFLOW = 10 11 12/** 13 * Inserts newlines in a string to wrap it at the specified width. 14 * Uses ANSI-aware slicing to avoid splitting escape sequences. 15 * @param text The text to wrap. 16 * @param wrapWidth The width at which to wrap lines (in visible characters). 17 * @returns The wrapped text. 18 */ 19function wrapText( 20 text: string, 21 wrapWidth: number, 22): { aboveTheFold: string; remainingLines: number } { 23 const lines = text.split('\n') 24 const wrappedLines: string[] = [] 25 26 for (const line of lines) { 27 const visibleWidth = stringWidth(line) 28 if (visibleWidth <= wrapWidth) { 29 wrappedLines.push(line.trimEnd()) 30 } else { 31 // Break long lines into chunks of wrapWidth visible characters 32 // using ANSI-aware slicing to preserve escape sequences 33 let position = 0 34 while (position < visibleWidth) { 35 const chunk = sliceAnsi(line, position, position + wrapWidth) 36 wrappedLines.push(chunk.trimEnd()) 37 position += wrapWidth 38 } 39 } 40 } 41 42 const remainingLines = wrappedLines.length - MAX_LINES_TO_SHOW 43 44 // If there's only 1 line after the fold, show it directly 45 // instead of showing "... +1 line (ctrl+o to expand)" 46 if (remainingLines === 1) { 47 return { 48 aboveTheFold: wrappedLines 49 .slice(0, MAX_LINES_TO_SHOW + 1) 50 .join('\n') 51 .trimEnd(), 52 remainingLines: 0, // All lines are shown, nothing remaining 53 } 54 } 55 56 // Otherwise show the standard MAX_LINES_TO_SHOW 57 return { 58 aboveTheFold: wrappedLines.slice(0, MAX_LINES_TO_SHOW).join('\n').trimEnd(), 59 remainingLines: Math.max(0, remainingLines), 60 } 61} 62 63/** 64 * Renders the content with line-based truncation for terminal display. 65 * If the content exceeds the maximum number of lines, it truncates the content 66 * and adds a message indicating the number of additional lines. 67 * @param content The content to render. 68 * @param terminalWidth Terminal width for wrapping lines. 69 * @returns The rendered content with truncation if needed. 70 */ 71export function renderTruncatedContent( 72 content: string, 73 terminalWidth: number, 74 suppressExpandHint = false, 75): string { 76 const trimmedContent = content.trimEnd() 77 if (!trimmedContent) { 78 return '' 79 } 80 81 const wrapWidth = Math.max(terminalWidth - PADDING_TO_PREVENT_OVERFLOW, 10) 82 83 // Only process enough content for the visible lines. Avoids O(n) wrapping 84 // on huge outputs (e.g. 64MB binary dumps that cause 382K-row screens). 85 const maxChars = MAX_LINES_TO_SHOW * wrapWidth * 4 86 const preTruncated = trimmedContent.length > maxChars 87 const contentForWrapping = preTruncated 88 ? trimmedContent.slice(0, maxChars) 89 : trimmedContent 90 91 const { aboveTheFold, remainingLines } = wrapText( 92 contentForWrapping, 93 wrapWidth, 94 ) 95 96 const estimatedRemaining = preTruncated 97 ? Math.max( 98 remainingLines, 99 Math.ceil(trimmedContent.length / wrapWidth) - MAX_LINES_TO_SHOW, 100 ) 101 : remainingLines 102 103 return [ 104 aboveTheFold, 105 estimatedRemaining > 0 106 ? chalk.dim( 107 `… +${estimatedRemaining} lines${suppressExpandHint ? '' : ` ${ctrlOToExpand()}`}`, 108 ) 109 : '', 110 ] 111 .filter(Boolean) 112 .join('\n') 113} 114 115/** Fast check: would OutputLine truncate this content? Counts raw newlines 116 * only (ignores terminal-width wrapping), so it may return false for a single 117 * very long line that wraps past 3 visual rows — acceptable, since the common 118 * case is multi-line output. */ 119export function isOutputLineTruncated(content: string): boolean { 120 let pos = 0 121 // Need more than MAX_LINES_TO_SHOW newlines (content fills > 3 lines). 122 // The +1 accounts for wrapText showing an extra line when remainingLines==1. 123 for (let i = 0; i <= MAX_LINES_TO_SHOW; i++) { 124 pos = content.indexOf('\n', pos) 125 if (pos === -1) return false 126 pos++ 127 } 128 // A trailing newline is a terminator, not a new line — match 129 // renderTruncatedContent's trimEnd() behavior. 130 return pos < content.length 131}