diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7fc96cb5e..a202b0454 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed the editor input box claiming a disproportionate share of small terminals (<=18 rows): the editor max-height floor (6 rows) ignored the available space. The height now yields to terminal size (`EDITOR_MIN_CHROME_ROWS`), always reserving rows for the transcript and status line. + ## [15.12.5] - 2026-06-13 ### Changed diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 221787c27..d7b9a1904 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -210,6 +210,14 @@ const EDITOR_MAX_HEIGHT_MIN = 6; const EDITOR_MAX_HEIGHT_MAX = 18; const EDITOR_RESERVED_ROWS = 12; const EDITOR_FALLBACK_ROWS = 24; +const EDITOR_MIN_CHROME_ROWS = 4; // rows reserved for transcript + status + chrome on tiny terms + +export function computeEditorMaxHeight(terminalRows: number): number { + const rows = Number.isFinite(terminalRows) && terminalRows > 0 ? terminalRows : EDITOR_FALLBACK_ROWS; + const desired = Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, rows - EDITOR_RESERVED_ROWS)); + // Never let the editor crowd out the rest of the UI on small terminals. + return Math.max(1, Math.min(desired, rows - EDITOR_MIN_CHROME_ROWS)); +} const HUD_NOTE_SUP_DIGITS: Record = { "0": "\u2070", @@ -1156,10 +1164,7 @@ export class InteractiveMode implements InteractiveModeContext { } #computeEditorMaxHeight(): number { - const rows = this.ui.terminal.rows; - const terminalRows = Number.isFinite(rows) && rows > 0 ? rows : EDITOR_FALLBACK_ROWS; - const maxHeight = terminalRows - EDITOR_RESERVED_ROWS; - return Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, maxHeight)); + return computeEditorMaxHeight(this.ui.terminal.rows); } #syncEditorMaxHeight(): void { diff --git a/packages/coding-agent/test/editor-max-height.test.ts b/packages/coding-agent/test/editor-max-height.test.ts new file mode 100644 index 000000000..0cee1945c --- /dev/null +++ b/packages/coding-agent/test/editor-max-height.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "bun:test"; +import { computeEditorMaxHeight } from "@oh-my-pi/pi-coding-agent/modes/interactive-mode"; + +describe("computeEditorMaxHeight", () => { + it("caps the editor while preserving chrome rows on small terminals", () => { + expect(computeEditorMaxHeight(30)).toBe(18); + expect(computeEditorMaxHeight(18)).toBe(6); + expect(computeEditorMaxHeight(8)).toBe(4); + expect(computeEditorMaxHeight(5)).toBe(1); + expect(computeEditorMaxHeight(Number.NaN)).toBe(12); + expect(computeEditorMaxHeight(0)).toBe(12); + + for (let rows = 5; rows <= 18; rows += 1) { + expect(rows - computeEditorMaxHeight(rows)).toBeGreaterThanOrEqual(4); + } + }); +}); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 4e7c142cb..bc1550b24 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed overlays without an explicit `maxHeight` dropping their bottom rows off-screen when taller than the terminal: `#resolveOverlayLayout` now defaults the height cap to the available rows, so a tall overlay is sliced to fit (and re-clamps on resize) instead of overflowing the visible region. + ## [15.12.5] - 2026-06-13 ### Added diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 8bc218193..4896989f3 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -1902,11 +1902,8 @@ export class TUI extends Container { width = Math.max(1, Math.min(width, availWidth)); // === Resolve maxHeight === - let maxHeight = parseSizeValue(opt.maxHeight, termHeight); - // Clamp to available space - if (maxHeight !== undefined) { - maxHeight = Math.max(1, Math.min(maxHeight, availHeight)); - } + let maxHeight = parseSizeValue(opt.maxHeight, termHeight) ?? availHeight; + maxHeight = Math.max(1, Math.min(maxHeight, availHeight)); // Effective overlay height (may be clamped by maxHeight) const effectiveHeight = maxHeight !== undefined ? Math.min(overlayHeight, maxHeight) : overlayHeight; diff --git a/packages/tui/test/overlay-scroll.test.ts b/packages/tui/test/overlay-scroll.test.ts index 9f4ef9811..b5f19cc34 100644 --- a/packages/tui/test/overlay-scroll.test.ts +++ b/packages/tui/test/overlay-scroll.test.ts @@ -140,6 +140,49 @@ describe("TUI overlays", () => { expect(term.getScrollBuffer().length).toBeLessThan(200); }); + it("clamps tall overlays without an explicit maxHeight to the available rows", async () => { + const term = new VirtualTerminal(80, 24); + const tui = new TUI(term); + + tui.addChild(new LineComponent("base-", 3)); + + tui.start(); + await flushRender(term); + + // A bottom margin reserves rows the overlay must NOT paint into. The overlay + // has no explicit maxHeight, so before the fix it rendered all 40 lines and + // the compositor only skipped rows past the terminal edge — ov-0..ov-(rows-1) + // were painted, including the reserved bottom band. The maxHeight=availHeight + // default slices the overlay to availHeight = rows - marginBottom. + const marginBottom = 6; + tui.showOverlay(new LineComponent("ov-", 40), { anchor: "top-center", margin: { bottom: marginBottom } }); + await flushRender(term); + + const maxVisibleOverlayIndex = (): number => { + let max = -1; + for (const line of term.getViewport()) { + const match = line.trim().match(/^ov-(\d+)$/); + if (!match) continue; + max = Math.max(max, Number.parseInt(match[1], 10)); + } + return max; + }; + + // availHeight = 24 - 6 = 18 → overlay sliced to ov-0..ov-17, nothing in the + // reserved bottom 6 rows. The old unclamped behavior surfaced ov-18..ov-23. + expect(maxVisibleOverlayIndex()).toBeGreaterThanOrEqual(0); + expect(maxVisibleOverlayIndex()).toBeLessThan(24 - marginBottom); + + term.resize(80, 10); + await settleResize(term); + + // availHeight = 10 - 6 = 4 → overlay re-clamped to ov-0..ov-3. + expect(maxVisibleOverlayIndex()).toBeGreaterThanOrEqual(0); + expect(maxVisibleOverlayIndex()).toBeLessThan(10 - marginBottom); + + tui.stop(); + }); + it("clears stale viewport content on launch", async () => { const term = new VirtualTerminal(40, 4); term.write("shell-0\r\nshell-1\r\nshell-2\r\nshell-3\r\nshell-4\r\n");