Skip to content

Terminal pane grows past the visible viewport and clips newest output (missing min-height clamp in ancestor flex chain) #1297

Description

@prashantsolanki3

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

  1. Open an SSH terminal session.
  2. 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).
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions