fix(tui): honest editor-height floor and dead-branch cleanup
Addresses Copilot + Codex review on #2521: - computeEditorMaxHeight returned 1 on terminals too small to host both the editor and the chrome reserve, but the bordered editor never renders fewer than 3 rows (2 border + 1 content). The cap now floors at that real minimum (EDITOR_MIN_RENDERED_ROWS) so it no longer misreports the rows the editor occupies; rendering is unchanged, and the contract is documented honestly (reserve holds once terminalRows >= 7). - #resolveOverlayLayout now always resolves maxHeight (?? availHeight), so the maxHeight !== undefined branch in effectiveHeight, the composite slice guard, and the number | undefined return type were dead. Tightened all three. - Mirrored the maxHeight-default contract in the render stress oracle (resolveExpectedOverlayLayout + compositeExpectedOverlays) so the randomized sweep validates the real clipped geometry instead of the obsolete unclipped one; updated the oracle helper test expectation accordingly. Editor-height tests rewritten to assert the real contract (reserve when the terminal can host both; pinned to the bordered minimum below that).
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
### 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.
|
||||
- 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`), reserving rows for the transcript and status line whenever the terminal can host both; on terminals too small for both, the editor collapses to its real bordered minimum instead of overshooting a fictitious cap.
|
||||
|
||||
## [15.12.5] - 2026-06-13
|
||||
### Changed
|
||||
|
||||
@@ -210,13 +210,24 @@ 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
|
||||
const EDITOR_MIN_CHROME_ROWS = 4; // rows reserved for transcript + status on small terms
|
||||
const EDITOR_MIN_RENDERED_ROWS = 3; // bordered editor floor: top+bottom border + 1 content row
|
||||
|
||||
/**
|
||||
* Editor max-height cap for a terminal of `terminalRows` rows.
|
||||
*
|
||||
* Roomy terminals get the comfortable [6, 18] band. Small terminals shrink the
|
||||
* cap so the editor leaves at least EDITOR_MIN_CHROME_ROWS rows for the
|
||||
* transcript + status line. The editor is bordered, so it never renders fewer
|
||||
* than EDITOR_MIN_RENDERED_ROWS rows; once the terminal is too small for both
|
||||
* (terminalRows < EDITOR_MIN_RENDERED_ROWS + EDITOR_MIN_CHROME_ROWS) the cap is
|
||||
* pinned to that floor — returning a smaller number would not shrink the editor
|
||||
* any further, it would only misreport the rows it actually occupies.
|
||||
*/
|
||||
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 comfortable = Math.max(EDITOR_MAX_HEIGHT_MIN, Math.min(EDITOR_MAX_HEIGHT_MAX, rows - EDITOR_RESERVED_ROWS));
|
||||
return Math.max(EDITOR_MIN_RENDERED_ROWS, Math.min(comfortable, rows - EDITOR_MIN_CHROME_ROWS));
|
||||
}
|
||||
|
||||
const HUD_NOTE_SUP_DIGITS: Record<string, string> = {
|
||||
|
||||
@@ -2,16 +2,28 @@ 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", () => {
|
||||
it("caps the editor within the comfortable band on roomy 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) {
|
||||
it("reserves at least four chrome rows once the terminal can host both", () => {
|
||||
// Editor floor (3 rendered rows, bordered) + chrome reserve (4) = 7 rows.
|
||||
for (let rows = 7; rows <= 18; rows += 1) {
|
||||
expect(rows - computeEditorMaxHeight(rows)).toBeGreaterThanOrEqual(4);
|
||||
}
|
||||
});
|
||||
|
||||
it("pins the cap to the bordered editor's real minimum on tinier terminals", () => {
|
||||
// Below 7 rows there is no room for both; the cap collapses to the editor's
|
||||
// real rendered floor (2 border + 1 content) rather than a fictitious value
|
||||
// the editor would silently overshoot.
|
||||
expect(computeEditorMaxHeight(6)).toBe(3);
|
||||
expect(computeEditorMaxHeight(5)).toBe(3);
|
||||
expect(computeEditorMaxHeight(4)).toBe(3);
|
||||
expect(computeEditorMaxHeight(1)).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1875,7 +1875,7 @@ export class TUI extends Container {
|
||||
overlayHeight: number,
|
||||
termWidth: number,
|
||||
termHeight: number,
|
||||
): { width: number; row: number; col: number; maxHeight: number | undefined } {
|
||||
): { width: number; row: number; col: number; maxHeight: number } {
|
||||
const opt = options ?? {};
|
||||
|
||||
// Parse margin (clamp to non-negative)
|
||||
@@ -1905,8 +1905,9 @@ export class TUI extends Container {
|
||||
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;
|
||||
// Effective overlay height: maxHeight is always resolved (defaults to
|
||||
// availHeight above), so the overlay is unconditionally clamped to fit.
|
||||
const effectiveHeight = Math.min(overlayHeight, maxHeight);
|
||||
|
||||
// === Resolve position ===
|
||||
let row: number;
|
||||
@@ -2017,7 +2018,7 @@ export class TUI extends Container {
|
||||
// (width and maxHeight don't depend on overlay height).
|
||||
const { width, maxHeight } = this.#resolveOverlayLayout(options, 0, termWidth, termHeight);
|
||||
let overlayLines = component.render(width);
|
||||
if (maxHeight !== undefined && overlayLines.length > maxHeight) {
|
||||
if (overlayLines.length > maxHeight) {
|
||||
overlayLines = overlayLines.slice(0, maxHeight);
|
||||
}
|
||||
const { row, col } = this.#resolveOverlayLayout(options, overlayLines.length, termWidth, termHeight);
|
||||
|
||||
@@ -3004,7 +3004,7 @@ function compositeExpectedOverlays(
|
||||
if (!isExpectedOverlayVisible(entry, termWidth, termHeight)) continue;
|
||||
const firstLayout = resolveExpectedOverlayLayout(entry.options, 0, termWidth, termHeight);
|
||||
let overlayLines = entry.component.render(firstLayout.width);
|
||||
if (firstLayout.maxHeight !== undefined && overlayLines.length > firstLayout.maxHeight) {
|
||||
if (overlayLines.length > firstLayout.maxHeight) {
|
||||
overlayLines = overlayLines.slice(0, firstLayout.maxHeight);
|
||||
}
|
||||
const layout = resolveExpectedOverlayLayout(entry.options, overlayLines.length, termWidth, termHeight);
|
||||
@@ -3039,7 +3039,7 @@ export function resolveExpectedOverlayLayout(
|
||||
overlayHeight: number,
|
||||
termWidth: number,
|
||||
termHeight: number,
|
||||
): { width: number; row: number; col: number; maxHeight: number | undefined } {
|
||||
): { width: number; row: number; col: number; maxHeight: number } {
|
||||
const opt = options ?? {};
|
||||
const margin =
|
||||
typeof opt.margin === "number"
|
||||
@@ -3056,11 +3056,9 @@ export function resolveExpectedOverlayLayout(
|
||||
width = Math.max(width, opt.minWidth);
|
||||
}
|
||||
width = Math.max(1, Math.min(width, availWidth));
|
||||
let maxHeight = parseOverlaySizeValue(opt.maxHeight, termHeight);
|
||||
if (maxHeight !== undefined) {
|
||||
maxHeight = Math.max(1, Math.min(maxHeight, availHeight));
|
||||
}
|
||||
const effectiveHeight = maxHeight !== undefined ? Math.min(overlayHeight, maxHeight) : overlayHeight;
|
||||
let maxHeight = parseOverlaySizeValue(opt.maxHeight, termHeight) ?? availHeight;
|
||||
maxHeight = Math.max(1, Math.min(maxHeight, availHeight));
|
||||
const effectiveHeight = Math.min(overlayHeight, maxHeight);
|
||||
let row: number;
|
||||
let col: number;
|
||||
if (opt.row !== undefined) {
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("render stress oracle helpers", () => {
|
||||
width: 10,
|
||||
row: 6,
|
||||
col: 29,
|
||||
maxHeight: undefined,
|
||||
maxHeight: 8,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user