Revert "fix(tui): clamp bogus terminal dimensions (WSL 131072x1) (#35657)" (#36096)

This reverts commit b1d34cf6e2.
This commit is contained in:
Teknium 2026-05-31 15:51:11 -07:00 committed by GitHub
parent 51c68d4ab1
commit cd8aa389c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 0 additions and 258 deletions

View file

@ -1,108 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
clampStdoutDimensions,
DEFAULT_COLUMNS,
DEFAULT_ROWS,
MAX_COLUMNS,
MAX_ROWS,
sanitizeDimension,
sanitizeTerminalSize
} from '../lib/terminalDimensions.js'
describe('sanitizeDimension', () => {
it('passes through an in-range value', () => {
expect(sanitizeDimension(120, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(120)
})
it('floors fractional values', () => {
expect(sanitizeDimension(80.9, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(80)
})
it('clamps an absurd width to the max, not the fallback', () => {
expect(sanitizeDimension(131072, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(MAX_COLUMNS)
})
it('falls back when value is zero', () => {
expect(sanitizeDimension(0, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(DEFAULT_COLUMNS)
})
it('falls back when value is negative', () => {
expect(sanitizeDimension(-5, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(DEFAULT_COLUMNS)
})
it('falls back on NaN / undefined / non-number', () => {
expect(sanitizeDimension(NaN, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(DEFAULT_COLUMNS)
expect(sanitizeDimension(undefined, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(DEFAULT_COLUMNS)
expect(sanitizeDimension('80', 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(DEFAULT_COLUMNS)
expect(sanitizeDimension(Infinity, 1, MAX_COLUMNS, DEFAULT_COLUMNS)).toBe(DEFAULT_COLUMNS)
})
})
describe('sanitizeTerminalSize', () => {
it('sanitizes the WSL 131072x1 report', () => {
// 131072 cols is absurd → clamp to max; 1 row is a valid (degenerate) TTY → keep.
expect(sanitizeTerminalSize(131072, 1)).toEqual({ columns: MAX_COLUMNS, rows: 1 })
})
it('passes a normal terminal through unchanged', () => {
expect(sanitizeTerminalSize(120, 40)).toEqual({ columns: 120, rows: 40 })
})
it('falls back when both dimensions are missing', () => {
expect(sanitizeTerminalSize(undefined, undefined)).toEqual({
columns: DEFAULT_COLUMNS,
rows: DEFAULT_ROWS
})
})
it('clamps an oversized height', () => {
expect(sanitizeTerminalSize(80, 99999)).toEqual({ columns: 80, rows: MAX_ROWS })
})
})
describe('clampStdoutDimensions', () => {
it('clamps a bogus columns getter on a live stream', () => {
let raw = 131072
const stream: { columns?: number; rows?: number } = {}
Object.defineProperty(stream, 'columns', { configurable: true, get: () => raw })
Object.defineProperty(stream, 'rows', { configurable: true, get: () => 1 })
clampStdoutDimensions(stream)
expect(stream.columns).toBe(MAX_COLUMNS)
expect(stream.rows).toBe(1)
// Live resize still propagates through the original getter, clamped.
raw = 100
expect(stream.columns).toBe(100)
raw = 0
expect(stream.columns).toBe(DEFAULT_COLUMNS)
})
it('clamps a bogus plain-value columns property', () => {
const stream: { columns?: number; rows?: number } = { columns: 131072, rows: 24 }
clampStdoutDimensions(stream)
expect(stream.columns).toBe(MAX_COLUMNS)
expect(stream.rows).toBe(24)
})
it('is idempotent', () => {
const stream: { columns?: number; rows?: number } = { columns: 131072, rows: 24 }
clampStdoutDimensions(stream)
clampStdoutDimensions(stream)
expect(stream.columns).toBe(MAX_COLUMNS)
})
it('does not crash on a non-configurable property', () => {
const stream: { columns?: number; rows?: number } = {}
Object.defineProperty(stream, 'columns', { configurable: false, value: 131072 })
expect(() => clampStdoutDimensions(stream)).not.toThrow()
})
})

View file

@ -12,7 +12,6 @@ import { formatBytes, type HeapDumpResult, performHeapDump } from './lib/memory.
import { type MemorySnapshot, startMemoryMonitor } from './lib/memoryMonitor.js'
import { openExternalUrl } from './lib/openExternalUrl.js'
import { recordParentLifecycle } from './lib/parentLog.js'
import { clampStdoutDimensions } from './lib/terminalDimensions.js'
import { resetTerminalModes } from './lib/terminalModes.js'
if (!process.stdin.isTTY) {
@ -20,12 +19,6 @@ if (!process.stdin.isTTY) {
process.exit(0)
}
// Some hosts (notably WSL) report bogus window sizes such as 131072x1. Clamp
// `process.stdout.columns`/`rows` at the source so the Ink renderer, its
// resize handler, and every component read see sane values. Must run before
// `ink.render` constructs the renderer.
clampStdoutDimensions()
// Start from a clean slate. If a previous TUI crashed or was kill -9'd, the
// terminal tab can still have mouse/focus/paste modes enabled.
resetTerminalModes()

View file

@ -1,143 +0,0 @@
/**
* Sanitize terminal dimensions reported by the host.
*
* Some environments report bogus window sizes. The motivating case (WSL,
* reported by @northframe_17) is `columns=131072, rows=1` a width that
* overflows any sane layout and a height of one row that makes the TUI
* unusable. Node's own `stdout.columns || 80` fallback only catches
* `0`/`NaN`/`undefined`, so a positive-but-absurd value sails straight into
* the Ink renderer, which then allocates a 131072-cell-wide screen buffer.
*
* We clamp each dimension independently to a sane range. Out-of-range or
* non-finite values fall back to the conventional 80x24 default rather than
* the raw garbage.
*/
export const DEFAULT_COLUMNS = 80
export const DEFAULT_ROWS = 24
// Upper bounds are generous (ultrawide multi-monitor terminals, tmux panes
// spanning huge displays) but well below the WSL garbage value. Anything
// beyond these is treated as a broken probe.
export const MAX_COLUMNS = 2000
export const MAX_ROWS = 1000
export const MIN_COLUMNS = 1
export const MIN_ROWS = 1
/**
* Clamp a single reported dimension into `[min, max]`.
*
* Returns `fallback` when the value is non-finite or `<= 0` (the classic
* "no size yet" signal). A positive value above `max` is clamped to `max`,
* not replaced by the fallback an oversized-but-finite report is more
* likely a real-but-large terminal than a missing one, and clamping keeps
* the layout sane either way.
*/
export function sanitizeDimension(value: unknown, min: number, max: number, fallback: number): number {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return fallback
}
const rounded = Math.floor(value)
if (rounded < min) {
return fallback
}
if (rounded > max) {
return max
}
return rounded
}
export interface SanitizedTerminalSize {
columns: number
rows: number
}
/** Sanitize a (columns, rows) pair using the TUI's bounds. */
export function sanitizeTerminalSize(columns: unknown, rows: unknown): SanitizedTerminalSize {
return {
columns: sanitizeDimension(columns, MIN_COLUMNS, MAX_COLUMNS, DEFAULT_COLUMNS),
rows: sanitizeDimension(rows, MIN_ROWS, MAX_ROWS, DEFAULT_ROWS)
}
}
interface ClampableStream {
columns?: number
rows?: number
}
const PATCHED = Symbol.for('hermes.tui.clampedDimensions')
/**
* Install clamping getters on `process.stdout` (or a provided stream) so every
* downstream reader the Ink renderer's root layout, its `resize` handler,
* and our React components' `stdout.columns ?? 80` reads sees sanitized
* values. Must run before `ink.render`.
*
* Idempotent: re-installing on an already-patched stream is a no-op. The raw
* values are read through the original property descriptor on each access, so
* live resizes still propagate (just clamped).
*/
export function clampStdoutDimensions(stream: ClampableStream = process.stdout): void {
const target = stream as ClampableStream & { [PATCHED]?: boolean }
if (target[PATCHED]) {
return
}
// Capture the original descriptors so we read the live host value on every
// access rather than freezing a single snapshot.
const colsDesc = findDescriptor(target, 'columns')
const rowsDesc = findDescriptor(target, 'rows')
const readCols = () => (colsDesc ? readValue(target, colsDesc) : target.columns)
const readRows = () => (rowsDesc ? readValue(target, rowsDesc) : target.rows)
try {
Object.defineProperty(target, 'columns', {
configurable: true,
enumerable: true,
get() {
return sanitizeDimension(readCols(), MIN_COLUMNS, MAX_COLUMNS, DEFAULT_COLUMNS)
}
})
Object.defineProperty(target, 'rows', {
configurable: true,
enumerable: true,
get() {
return sanitizeDimension(readRows(), MIN_ROWS, MAX_ROWS, DEFAULT_ROWS)
}
})
target[PATCHED] = true
} catch {
// Non-configurable property on an exotic stream — leave it alone rather
// than crashing startup. Components still have their own `?? 80` guard.
}
}
function findDescriptor(obj: object, key: string): PropertyDescriptor | undefined {
let cur: object | null = obj
while (cur) {
const desc = Object.getOwnPropertyDescriptor(cur, key)
if (desc) {
return desc
}
cur = Object.getPrototypeOf(cur) as object | null
}
return undefined
}
function readValue(target: object, desc: PropertyDescriptor): unknown {
if (desc.get) {
return desc.get.call(target)
}
return desc.value
}