From 63f6a497cba61299a1c24aa96795a55479740ac6 Mon Sep 17 00:00:00 2001
From: Jacob Richman <jacob314@gmail.com>
Date: Sun, 22 Jun 2025 00:54:10 +0000
Subject: Jacob314/overflow notification and one MaxSizedBox bug fix (#1288)

---
 .../cli/src/ui/components/shared/MaxSizedBox.tsx   | 101 +++++++++++++--------
 1 file changed, 65 insertions(+), 36 deletions(-)

(limited to 'packages/cli/src/ui/components/shared/MaxSizedBox.tsx')

diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
index 1b5b90aa..faa1052a 100644
--- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
+++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx
@@ -4,14 +4,22 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import React, { Fragment } from 'react';
+import React, { Fragment, useEffect, useId } from 'react';
 import { Box, Text } from 'ink';
 import stringWidth from 'string-width';
 import { Colors } from '../../colors.js';
 import { toCodePoints } from '../../utils/textUtils.js';
+import { useOverflowActions } from '../../contexts/OverflowContext.js';
 
 let enableDebugLog = false;
 
+/**
+ * Minimum height for the MaxSizedBox component.
+ * This ensures there is room for at least one line of content as well as the
+ * message that content was truncated.
+ */
+export const MINIMUM_MAX_HEIGHT = 2;
+
 export function setMaxSizedBoxDebugging(value: boolean) {
   enableDebugLog = value;
 }
@@ -95,6 +103,10 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
   overflowDirection = 'top',
   additionalHiddenLinesCount = 0,
 }) => {
+  const id = useId();
+  const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
+
+  const laidOutStyledText: StyledText[][] = [];
   // When maxHeight is not set, we render the content normally rather
   // than using our custom layout logic. This should slightly improve
   // performance for the case where there is no height limit and is
@@ -103,54 +115,71 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
   // In the future we might choose to still apply our layout logic
   // even in this case particularlly if there are cases where we
   // intentionally diverse how certain layouts are rendered.
-  if (maxHeight === undefined) {
-    return (
-      <Box width={maxWidth} height={maxHeight} flexDirection="column">
-        {children}
-      </Box>
-    );
-  }
-
-  if (maxWidth === undefined) {
-    throw new Error('maxWidth must be defined when maxHeight is set.');
-  }
+  let targetMaxHeight;
+  if (maxHeight !== undefined) {
+    targetMaxHeight = Math.max(Math.round(maxHeight), MINIMUM_MAX_HEIGHT);
 
-  const laidOutStyledText: StyledText[][] = [];
-  function visitRows(element: React.ReactNode) {
-    if (!React.isValidElement(element)) {
-      return;
+    if (maxWidth === undefined) {
+      throw new Error('maxWidth must be defined when maxHeight is set.');
     }
-    if (element.type === Fragment) {
-      React.Children.forEach(element.props.children, visitRows);
-      return;
-    }
-    if (element.type === Box) {
-      layoutInkElementAsStyledText(element, maxWidth!, laidOutStyledText);
-      return;
+    function visitRows(element: React.ReactNode) {
+      if (!React.isValidElement(element)) {
+        return;
+      }
+      if (element.type === Fragment) {
+        React.Children.forEach(element.props.children, visitRows);
+        return;
+      }
+      if (element.type === Box) {
+        layoutInkElementAsStyledText(element, maxWidth!, laidOutStyledText);
+        return;
+      }
+
+      debugReportError('MaxSizedBox children must be <Box> elements', element);
     }
 
-    debugReportError('MaxSizedBox children must be <Box> elements', element);
+    React.Children.forEach(children, visitRows);
   }
 
-  React.Children.forEach(children, visitRows);
-
   const contentWillOverflow =
-    (laidOutStyledText.length > maxHeight && maxHeight > 0) ||
+    (targetMaxHeight !== undefined &&
+      laidOutStyledText.length > targetMaxHeight) ||
     additionalHiddenLinesCount > 0;
-  const visibleContentHeight = contentWillOverflow ? maxHeight - 1 : maxHeight;
-
-  const hiddenLinesCount = Math.max(
-    0,
-    laidOutStyledText.length - visibleContentHeight,
-  );
+  const visibleContentHeight =
+    contentWillOverflow && targetMaxHeight !== undefined
+      ? targetMaxHeight - 1
+      : targetMaxHeight;
+
+  const hiddenLinesCount =
+    visibleContentHeight !== undefined
+      ? Math.max(0, laidOutStyledText.length - visibleContentHeight)
+      : 0;
   const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
 
+  useEffect(() => {
+    if (totalHiddenLines > 0) {
+      addOverflowingId?.(id);
+    } else {
+      removeOverflowingId?.(id);
+    }
+
+    return () => {
+      removeOverflowingId?.(id);
+    };
+  }, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
+
+  if (maxHeight === undefined) {
+    return (
+      <Box width={maxWidth} height={maxHeight} flexDirection="column">
+        {children}
+      </Box>
+    );
+  }
+
   const visibleStyledText =
     hiddenLinesCount > 0
       ? overflowDirection === 'top'
-        ? laidOutStyledText.slice(
-            laidOutStyledText.length - visibleContentHeight,
-          )
+        ? laidOutStyledText.slice(hiddenLinesCount, laidOutStyledText.length)
         : laidOutStyledText.slice(0, visibleContentHeight)
       : laidOutStyledText;
 
-- 
cgit v1.2.3

