From 4a3b7551627513c0d0d7f9d37917c8b2683fb6d2 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Thu, 11 Jun 2026 14:12:50 +0530 Subject: [PATCH] =?UTF-8?q?opentui(v6):=20restore=20scrollback=20cap=20100?= =?UTF-8?q?0=20=E2=86=92=203000=20under=20windowing=20(#27=20payoff)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With transcript windowing (S1+S2) the mounted set no longer scales with the store (peak 31 rows over a 1500-row burst), so the handle-table clamp that forced 1000 rows is unnecessary when windowing is on. The ceiling is now windowing-aware: 3000 rows (the originally-shipped default, regression documented in opentui-fixes-audit.md §2) with windowing, 1000 with HERMES_TUI_WINDOWING=0 (every row mounts again). Measured at the restored cap (full 3000-msg store): mem3000 360MB peak styled end-to-end (pre-campaign: ~870MB + unstyled past ~1,400 rows; before that: crash). scroll3000 p50=2 p90=3 p99=8 max=17ms (Ink same workload: p90=35 p99=96). Gate digest unchanged. --- ui-opentui/src/logic/store.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/ui-opentui/src/logic/store.ts b/ui-opentui/src/logic/store.ts index 6b1afd353be..79d1b978d0e 100644 --- a/ui-opentui/src/logic/store.ts +++ b/ui-opentui/src/logic/store.ts @@ -24,7 +24,7 @@ import { import type { DetailsMode } from './details.ts' import { diffStats, type DiffStats } from './diff.ts' import type { SessionTabId } from './sessionPicker.ts' -import { envOutputUnlimited } from './env.ts' +import { envFlag, envOutputUnlimited } from './env.ts' import { registerNotifier } from './notify.ts' import { stripAnsi, stripOmittedNote, stripToolEnvelope } from './toolOutput.ts' import { DEFAULT_THEME, type Theme, themeFromSkin } from './theme.ts' @@ -411,17 +411,27 @@ export function createSessionStore(options?: SessionStoreOptions) { // HANDLE_SAFE_MAX_ROWS = 1000 ≈ 47k handles ≈ 72% of the table on the // realistic-fixture mix, leaving ~18k slots of headroom for chrome // (composer/pickers/dashboard) and heavier-than-fixture rows. Pathological - // rows can still exceed it — renderable-weight-aware capping belongs to the - // virtualization work (#27); until then nativeHandles.ts degrades (unstyled - // text) instead of crashing. `HERMES_TUI_MAX_MESSAGES` can LOWER the cap but + // rows can still exceed it; nativeHandles.ts degrades (unstyled text) + // instead of crashing. `HERMES_TUI_MAX_MESSAGES` can LOWER the cap but // never raise it past the ceiling. Read once per store. Trimmed turns aren't // lost — they live on the gateway and are recoverable via `/resume`. + // + // With transcript WINDOWING on (#27, S1+S2 in view/transcript.tsx — the + // default), handles no longer scale with the store: out-of-window rows are + // exact-height spacers and the mounted set is ~3 viewports (measured peak 31 + // rows over a 1500-row burst), so the scrollback ceiling returns to 3000 + // (the originally shipped default, regression documented in + // docs/plans/opentui-fixes-audit.md §2). The HERMES_TUI_WINDOWING=0 escape + // hatch mounts every row again, so it keeps the handle-safe 1000 clamp. const HANDLE_SAFE_MAX_ROWS = 1000 + const WINDOWED_MAX_ROWS = 3000 const MESSAGE_CAP = (() => { if (options?.uncappedFixture) return Number.MAX_SAFE_INTEGER + const windowing = envFlag(process.env.HERMES_TUI_WINDOWING, true) + const ceiling = windowing ? WINDOWED_MAX_ROWS : HANDLE_SAFE_MAX_ROWS const raw = Number.parseInt(process.env.HERMES_TUI_MAX_MESSAGES ?? '', 10) - const requested = Number.isFinite(raw) && raw > 0 ? raw : HANDLE_SAFE_MAX_ROWS - return Math.min(requested, HANDLE_SAFE_MAX_ROWS) + const requested = Number.isFinite(raw) && raw > 0 ? raw : ceiling + return Math.min(requested, ceiling) })() const [state, setState] = createStore({