Summary
Once terminal output has filled the pane's initial viewport once, the newest rows (bottom of the terminal — e.g. an interactive program's status/input line) become invisible. It reproduces on any later event that changes the available height/width for the pane: collapsing/expanding the sidebar, browser zoom, resizing the window. The terminal never shrinks back down to match the actually-visible area.
Environment
- Self-hosted, Docker deployment (
ghcr.io/lukegus/termix)
- Reproduced by reading source at both
release-2.6.1-tag and release-2.7.1-tag (current latest as of filing) — the relevant code is byte-identical between the two, so this affects the current release.
- Browser-based web UI (not desktop/Electron).
Steps to reproduce
- Open an SSH terminal session.
- Run a command that produces enough output to fill the visible pane (e.g.
ls -la on a large directory, or start an interactive program with a status/input bar at the bottom, like a REPL or an interactive CLI tool).
- Once the pane is full, trigger any layout change that should shrink the available height/width for the terminal — e.g. collapse/expand the app sidebar, or change browser zoom level.
- Observe: the terminal pane does not re-fit to the new visible area. The bottom rows (most recent output, or an interactive program's input line) are clipped/hidden rather than the terminal reflowing to fewer rows.
Root cause (read from source, not guessed)
src/ui/features/terminal/Terminal.tsx wires up FitAddon + a debounced ResizeObserver that watches xtermRef.current.parentElement ?? xtermRef.current and calls fitAddon.fit() on change (confirmed present, unchanged, at both release-2.6.1-tag and release-2.7.1-tag):
const resizeObserver = new ResizeObserver(() => {
if (!isVisibleRef.current) return;
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
resizeTimeout.current = setTimeout(() => {
if (isVisibleRef.current) {
performFit();
}
}, 50);
});
const observeTarget = xtermRef.current.parentElement ?? xtermRef.current;
resizeObserver.observe(observeTarget);
This logic itself looks correct. The bug is in the CSS layout around it: both the terminal's outer wrapper <div> and the xtermRef mount <div> use:
className="h-full w-full relative"
(confirmed at release-2.6.1-tag lines ~3124 and ~3145; at release-2.7.1-tag lines ~3358 and ~3380 — same classes, same structure).
h-full (Tailwind's height: 100%) only clamps to the visible viewport if every flex ancestor between this wrapper and the actual viewport-bounded container sets min-height: 0. By default a flex item's min-height is auto, which means the browser refuses to shrink it below the size of its content. So once the terminal's rendered content (rows already printed) makes the pane want to be taller than the viewport, the pane — and the very element the ResizeObserver is watching — grows to match the content instead of staying clamped to the actual visible area. Whatever ancestor further up finally does impose a hard boundary (the browser window, or an overflow: hidden container) then clips the excess. Because the observed box never reports a shrink relative to the viewport (it only ever grew, then got visually clipped by an ancestor), fit() is never re-run with a smaller row count, so the terminal keeps believing it has more visible rows than it actually does.
I was not able to fully trace which specific ancestor in the tab/pane/split-view layout is missing the min-h-0 class (that logic is spread across the shell/pane layout components, not Terminal.tsx itself), but the pattern — plain h-full/w-full with no min-h-0 anywhere in the two levels closest to the terminal — is the standard signature of this exact bug class in flexbox layouts hosting a fixed-size canvas like xterm.js.
Suggested fix direction
Audit the flex ancestor chain from the app shell down through the tab/pane content area to Terminal.tsx's wrapper, and add min-h-0 (and min-w-0 if the same issue affects horizontal fit) at each flex-1/flex-col/flex-row level in that chain. Alternatively, give the outermost scrollable/clamped pane an explicit height derived from a measured value (or 100dvh-based sizing) with overflow: hidden, rather than relying purely on h-full inheritance through an unconstrained flex chain.
Happy to test a fix against a self-hosted instance if useful.
Summary
Once terminal output has filled the pane's initial viewport once, the newest rows (bottom of the terminal — e.g. an interactive program's status/input line) become invisible. It reproduces on any later event that changes the available height/width for the pane: collapsing/expanding the sidebar, browser zoom, resizing the window. The terminal never shrinks back down to match the actually-visible area.
Environment
ghcr.io/lukegus/termix)release-2.6.1-tagandrelease-2.7.1-tag(current latest as of filing) — the relevant code is byte-identical between the two, so this affects the current release.Steps to reproduce
ls -laon a large directory, or start an interactive program with a status/input bar at the bottom, like a REPL or an interactive CLI tool).Root cause (read from source, not guessed)
src/ui/features/terminal/Terminal.tsxwires upFitAddon+ a debouncedResizeObserverthat watchesxtermRef.current.parentElement ?? xtermRef.currentand callsfitAddon.fit()on change (confirmed present, unchanged, at bothrelease-2.6.1-tagandrelease-2.7.1-tag):This logic itself looks correct. The bug is in the CSS layout around it: both the terminal's outer wrapper
<div>and thextermRefmount<div>use:(confirmed at
release-2.6.1-taglines ~3124 and ~3145; atrelease-2.7.1-taglines ~3358 and ~3380 — same classes, same structure).h-full(Tailwind'sheight: 100%) only clamps to the visible viewport if every flex ancestor between this wrapper and the actual viewport-bounded container setsmin-height: 0. By default a flex item'smin-heightisauto, which means the browser refuses to shrink it below the size of its content. So once the terminal's rendered content (rows already printed) makes the pane want to be taller than the viewport, the pane — and the very element theResizeObserveris watching — grows to match the content instead of staying clamped to the actual visible area. Whatever ancestor further up finally does impose a hard boundary (the browser window, or anoverflow: hiddencontainer) then clips the excess. Because the observed box never reports a shrink relative to the viewport (it only ever grew, then got visually clipped by an ancestor),fit()is never re-run with a smaller row count, so the terminal keeps believing it has more visible rows than it actually does.I was not able to fully trace which specific ancestor in the tab/pane/split-view layout is missing the
min-h-0class (that logic is spread across the shell/pane layout components, notTerminal.tsxitself), but the pattern — plainh-full/w-fullwith nomin-h-0anywhere in the two levels closest to the terminal — is the standard signature of this exact bug class in flexbox layouts hosting a fixed-size canvas like xterm.js.Suggested fix direction
Audit the flex ancestor chain from the app shell down through the tab/pane content area to
Terminal.tsx's wrapper, and addmin-h-0(andmin-w-0if the same issue affects horizontal fit) at eachflex-1/flex-col/flex-rowlevel in that chain. Alternatively, give the outermost scrollable/clamped pane an explicit height derived from a measured value (or100dvh-based sizing) withoverflow: hidden, rather than relying purely onh-fullinheritance through an unconstrained flex chain.Happy to test a fix against a self-hosted instance if useful.