π File detail
utils/terminal.ts
π§© .tsπ 132 linesπΎ 4,372 bytesπ text
β Back to All Filesπ― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes renderTruncatedContent and isOutputLineTruncated β mainly functions, hooks, or classes. Dependencies touch terminal styling. It composes internal code from components, ink, and sliceAnsi (relative imports).
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
import chalk from 'chalk' import { ctrlOToExpand } from '../components/CtrlOToExpand.js' import { stringWidth } from '../ink/stringWidth.js' import sliceAnsi from './sliceAnsi.js'
π€ Exports (heuristic)
renderTruncatedContentisOutputLineTruncated
π External import roots
Package roots from from "β¦" (relative paths omitted).
chalk
π₯οΈ Source preview
import chalk from 'chalk'
import { ctrlOToExpand } from '../components/CtrlOToExpand.js'
import { stringWidth } from '../ink/stringWidth.js'
import sliceAnsi from './sliceAnsi.js'
// Text rendering utilities for terminal display
const MAX_LINES_TO_SHOW = 3
// Account for MessageResponse prefix (" βΏ " = 5 chars) + parent width
// reduction (columns - 5 in tool result rendering)
const PADDING_TO_PREVENT_OVERFLOW = 10
/**
* Inserts newlines in a string to wrap it at the specified width.
* Uses ANSI-aware slicing to avoid splitting escape sequences.
* @param text The text to wrap.
* @param wrapWidth The width at which to wrap lines (in visible characters).
* @returns The wrapped text.
*/
function wrapText(
text: string,
wrapWidth: number,
): { aboveTheFold: string; remainingLines: number } {
const lines = text.split('\n')
const wrappedLines: string[] = []
for (const line of lines) {
const visibleWidth = stringWidth(line)
if (visibleWidth <= wrapWidth) {
wrappedLines.push(line.trimEnd())
} else {
// Break long lines into chunks of wrapWidth visible characters
// using ANSI-aware slicing to preserve escape sequences
let position = 0
while (position < visibleWidth) {
const chunk = sliceAnsi(line, position, position + wrapWidth)
wrappedLines.push(chunk.trimEnd())
position += wrapWidth
}
}
}
const remainingLines = wrappedLines.length - MAX_LINES_TO_SHOW
// If there's only 1 line after the fold, show it directly
// instead of showing "... +1 line (ctrl+o to expand)"
if (remainingLines === 1) {
return {
aboveTheFold: wrappedLines
.slice(0, MAX_LINES_TO_SHOW + 1)
.join('\n')
.trimEnd(),
remainingLines: 0, // All lines are shown, nothing remaining
}
}
// Otherwise show the standard MAX_LINES_TO_SHOW
return {
aboveTheFold: wrappedLines.slice(0, MAX_LINES_TO_SHOW).join('\n').trimEnd(),
remainingLines: Math.max(0, remainingLines),
}
}
/**
* Renders the content with line-based truncation for terminal display.
* If the content exceeds the maximum number of lines, it truncates the content
* and adds a message indicating the number of additional lines.
* @param content The content to render.
* @param terminalWidth Terminal width for wrapping lines.
* @returns The rendered content with truncation if needed.
*/
export function renderTruncatedContent(
content: string,
terminalWidth: number,
suppressExpandHint = false,
): string {
const trimmedContent = content.trimEnd()
if (!trimmedContent) {
return ''
}
const wrapWidth = Math.max(terminalWidth - PADDING_TO_PREVENT_OVERFLOW, 10)
// Only process enough content for the visible lines. Avoids O(n) wrapping
// on huge outputs (e.g. 64MB binary dumps that cause 382K-row screens).
const maxChars = MAX_LINES_TO_SHOW * wrapWidth * 4
const preTruncated = trimmedContent.length > maxChars
const contentForWrapping = preTruncated
? trimmedContent.slice(0, maxChars)
: trimmedContent
const { aboveTheFold, remainingLines } = wrapText(
contentForWrapping,
wrapWidth,
)
const estimatedRemaining = preTruncated
? Math.max(
remainingLines,
Math.ceil(trimmedContent.length / wrapWidth) - MAX_LINES_TO_SHOW,
)
: remainingLines
return [
aboveTheFold,
estimatedRemaining > 0
? chalk.dim(
`β¦ +${estimatedRemaining} lines${suppressExpandHint ? '' : ` ${ctrlOToExpand()}`}`,
)
: '',
]
.filter(Boolean)
.join('\n')
}
/** Fast check: would OutputLine truncate this content? Counts raw newlines
* only (ignores terminal-width wrapping), so it may return false for a single
* very long line that wraps past 3 visual rows β acceptable, since the common
* case is multi-line output. */
export function isOutputLineTruncated(content: string): boolean {
let pos = 0
// Need more than MAX_LINES_TO_SHOW newlines (content fills > 3 lines).
// The +1 accounts for wrapText showing an extra line when remainingLines==1.
for (let i = 0; i <= MAX_LINES_TO_SHOW; i++) {
pos = content.indexOf('\n', pos)
if (pos === -1) return false
pos++
}
// A trailing newline is a terminator, not a new line β match
// renderTruncatedContent's trimEnd() behavior.
return pos < content.length
}