terminal.ts
utils/terminal.ts
No strong subsystem tag
132
Lines
4372
Bytes
2
Exports
4
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 132 lines, 4 detected imports, and 2 detected exports.
Important relationships
Detected exports
renderTruncatedContentisOutputLineTruncated
Keywords
contentlineswrapwidthremaininglinesmax_lines_to_showlinetextwrappedlinestrimendtrimmedcontent
Detected imports
chalk../components/CtrlOToExpand.js../ink/stringWidth.js./sliceAnsi.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
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
}