diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 50a99e2325d..332aca961eb 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -1,11 +1,12 @@ import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink' import { useStore } from '@nanostores/react' -import { memo } from 'react' +import { Fragment, memo } from 'react' import { useGateway } from '../app/gatewayContext.js' import type { AppLayoutProps } from '../app/interfaces.js' import { $isBlocked, $overlayState, patchOverlayState } from '../app/overlayStore.js' import { $uiState } from '../app/uiStore.js' +import { INLINE_MODE } from '../config/env.js' import { PLACEHOLDER } from '../content/placeholders.js' import { inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js' import { PerfPane } from '../lib/perfPane.js' @@ -244,8 +245,23 @@ export const AppLayout = memo(function AppLayout({ }: AppLayoutProps) { const overlay = useStore($overlayState) + // Inline mode: skip so the TUI renders into the + // primary buffer and the terminal's native scrollback can capture rows + // that scroll off the top. Mouse tracking is still enabled via + // AlternateScreen when the wrapper is on; in inline mode we leave it + // to the host terminal, which typically does wheel → scrollback. + // + // `Fragment` (via alias so the JSX stays legible) drops the alt-screen + // constraint while keeping the inner layout identical. Content height + // will then follow flex-column growth, which means the ScrollBox below + // grows beyond the viewport — the terminal's primary buffer scrolls + // old rows off the top into native scrollback. Composer + progress + // stay at the bottom via normal flow (they're the last siblings). + const Shell = INLINE_MODE ? Fragment : AlternateScreen + const shellProps = INLINE_MODE ? {} : { mouseTracking } + return ( - + {overlay.agents ? ( @@ -277,6 +293,6 @@ export const AppLayout = memo(function AppLayout({ )} - + ) }) diff --git a/ui-tui/src/config/env.ts b/ui-tui/src/config/env.ts index 60f1e80c539..96de9a99fe1 100644 --- a/ui-tui/src/config/env.ts +++ b/ui-tui/src/config/env.ts @@ -1,3 +1,14 @@ export const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim() export const MOUSE_TRACKING = !/^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_DISABLE_MOUSE ?? '').trim()) export const NO_CONFIRM_DESTRUCTIVE = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_NO_CONFIRM ?? '').trim()) +// Inline mode: skip the alt-screen wrapper. The TUI renders into the +// primary buffer so the terminal's native scrollback captures whatever +// scrolls off the top. Wheel + PageUp are then handled by the host +// terminal, not by our virtual-scroll logic. The live composer/progress +// area still pins to the bottom via Ink's normal flow. +// +// This is an experiment gate — the full "inline layout" (plain-text +// transcript with composer pinned below) is a bigger change; the env var +// here just disables AlternateScreen so we can measure whether native +// scrolling beats our virtualization on the same pipeline. +export const INLINE_MODE = /^(?:1|true|yes|on)$/i.test((process.env.HERMES_TUI_INLINE ?? '').trim())