diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
index 50951b4f..92147d3c 100644
--- a/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.test.tsx
@@ -248,6 +248,89 @@ Line 3`);
🐶`);
});
+ it('falls back to an ellipsis when width is extremely small', () => {
+ const { lastFrame } = render(
+
+
+
+ No
+ wrap
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals('N…');
+ });
+
+ it('truncates long non-wrapping text with ellipsis', () => {
+ const { lastFrame } = render(
+
+
+
+ ABCDE
+ wrap
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals('AB…');
+ });
+
+ it('truncates non-wrapping text containing line breaks', () => {
+ const { lastFrame } = render(
+
+
+
+ {'A\nBCDE'}
+ wrap
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals(`A\n…`);
+ });
+
+ it('truncates emoji characters correctly with ellipsis', () => {
+ const { lastFrame } = render(
+
+
+
+ 🐶🐶🐶
+ wrap
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals(`🐶…`);
+ });
+
+ it('shows ellipsis for multiple rows with long non-wrapping text', () => {
+ const { lastFrame } = render(
+
+
+
+ AAA
+ first
+
+
+ BBB
+ second
+
+
+ CCC
+ third
+
+
+ ,
+ );
+
+ expect(lastFrame()).equals(`AA…\nBB…\nCC…`);
+ });
+
it('accounts for additionalHiddenLinesCount', () => {
const { lastFrame } = render(
diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
index eb5ef6b4..346472bf 100644
--- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
@@ -432,8 +432,85 @@ function layoutInkElementAsStyledText(
const availableWidth = maxWidth - noWrappingWidth;
if (availableWidth < 1) {
- // No room to render the wrapping segments. TODO(jacob314): consider an alternative fallback strategy.
- output.push(nonWrappingContent);
+ // No room to render the wrapping segments. Truncate the non-wrapping
+ // content and append an ellipsis so the line always fits within maxWidth.
+
+ // Handle line breaks in non-wrapping content when truncating
+ const lines: StyledText[][] = [];
+ let currentLine: StyledText[] = [];
+ let currentLineWidth = 0;
+
+ for (const segment of nonWrappingContent) {
+ const textLines = segment.text.split('\n');
+ textLines.forEach((text, index) => {
+ if (index > 0) {
+ // New line encountered, finish current line and start new one
+ lines.push(currentLine);
+ currentLine = [];
+ currentLineWidth = 0;
+ }
+
+ if (text) {
+ const textWidth = stringWidth(text);
+
+ // When there's no room for wrapping content, be very conservative
+ // For lines after the first line break, show only ellipsis if the text would be truncated
+ if (index > 0 && textWidth > 0) {
+ // This is content after a line break - just show ellipsis to indicate truncation
+ currentLine.push({ text: '…', props: {} });
+ currentLineWidth = stringWidth('…');
+ } else {
+ // This is the first line or a continuation, try to fit what we can
+ const maxContentWidth = Math.max(0, maxWidth - stringWidth('…'));
+
+ if (textWidth <= maxContentWidth && currentLineWidth === 0) {
+ // Text fits completely on this line
+ currentLine.push({ text, props: segment.props });
+ currentLineWidth += textWidth;
+ } else {
+ // Text needs truncation
+ const codePoints = toCodePoints(text);
+ let truncatedWidth = currentLineWidth;
+ let sliceEndIndex = 0;
+
+ for (const char of codePoints) {
+ const charWidth = stringWidth(char);
+ if (truncatedWidth + charWidth > maxContentWidth) {
+ break;
+ }
+ truncatedWidth += charWidth;
+ sliceEndIndex++;
+ }
+
+ const slice = codePoints.slice(0, sliceEndIndex).join('');
+ if (slice) {
+ currentLine.push({ text: slice, props: segment.props });
+ }
+ currentLine.push({ text: '…', props: {} });
+ currentLineWidth = truncatedWidth + stringWidth('…');
+ }
+ }
+ }
+ });
+ }
+
+ // Add the last line if it has content or if the last segment ended with \n
+ if (
+ currentLine.length > 0 ||
+ (nonWrappingContent.length > 0 &&
+ nonWrappingContent[nonWrappingContent.length - 1].text.endsWith('\n'))
+ ) {
+ lines.push(currentLine);
+ }
+
+ // If we don't have any lines yet, add an ellipsis line
+ if (lines.length === 0) {
+ lines.push([{ text: '…', props: {} }]);
+ }
+
+ for (const line of lines) {
+ output.push(line);
+ }
return;
}