fix(tui): keep Main header visible in single-column mode

In StatusModeSingle (cols<80 || rows<20), an active panel whose
intrinsic content exceeds the container could push the frame past
terminal rows under tmux, scrolling the header off the top. Yoga
defaults flexShrink to 0 (unlike CSS), so panels refused to shrink
when wrapped Text grew them vertically.

- Default Panel flexShrink to 1 so panels collapse to fit container
- Pin StatusModeSingle's three rows to explicit heights with
  overflow:hidden, eliminating overflow paths entirely
- Pass availableRows to LogsPanel from the capped middleHeight so
  its rowBudget matches the actual height instead of stdout.rows-11

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-27 05:16:25 -07:00
parent 9a72efa306
commit da3da74f6b

View File

@@ -181,16 +181,20 @@ function Panel({ title, isFocused, children, flexGrow, flexShrink, width }: Pane
borderColor={isFocused ? "cyanBright" : "gray"}
flexDirection="column"
flexGrow={flexGrow}
flexShrink={flexShrink}
// Default flexShrink to 1. Yoga's default is 0 (unlike CSS), so without
// this a panel whose intrinsic content height exceeds its container —
// common at narrow widths where Text wraps — refuses to shrink, the
// frame grows past terminal rows, and Ink scrolls the top (header) off.
flexShrink={flexShrink ?? 1}
width={width}
overflow="hidden"
>
<Box paddingX={1}>
<Box paddingX={1} flexShrink={0}>
<Text bold={isFocused} color={isFocused ? "cyanBright" : undefined} dimColor={!isFocused}>
{title}
</Text>
</Box>
<Box flexDirection="column" paddingX={1} flexGrow={1} overflow="hidden">
<Box flexDirection="column" paddingX={1} flexGrow={1} flexShrink={1} overflow="hidden">
{children}
</Box>
</Box>
@@ -795,11 +799,19 @@ function StatusModeSingle({
controller: DashboardTUI;
}) {
const focused = state.activeSection;
const { stdout } = useStdout();
// Cap the middle pane so a panel's intrinsic content height (e.g. LogsPanel's
// own rowBudget computation) can never exceed terminal rows. Without this,
// Ink writes a frame taller than the terminal under tmux narrow panes and the
// top row (header) scrolls off — and stays off across re-renders.
const rows = stdout?.rows ?? 24;
const cols = stdout?.columns ?? 80;
const middleHeight = Math.max(1, rows - 2);
const activePanel = () => {
switch (focused) {
case "system": return <SystemPanel state={state} isFocused />;
case "logs": return <LogsPanel state={state} isFocused />;
case "logs": return <LogsPanel state={state} isFocused availableRows={middleHeight - 4} />;
case "utilities": return <UtilitiesPanel state={state} isFocused />;
case "stats": return <StatsPanel state={state} isFocused />;
case "settings": return <SettingsPanel state={state} isFocused />;
@@ -807,14 +819,14 @@ function StatusModeSingle({
};
return (
<Box flexDirection="column" flexGrow={1}>
<Box flexShrink={0}>
<Box flexDirection="column" height={rows} width={cols} overflow="hidden">
<Box height={1} flexShrink={0} overflow="hidden">
<MainHeader state={state} />
</Box>
<Box flexGrow={1} flexDirection="column" overflow="hidden">
<Box height={middleHeight} flexShrink={0} flexDirection="column" overflow="hidden">
{activePanel()}
</Box>
<Box flexShrink={0}>
<Box height={1} flexShrink={0} overflow="hidden">
<StatusBar state={state} controller={controller} />
</Box>
</Box>