mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
experiment(tui): HERMES_TUI_INLINE flag to skip AlternateScreen
Adds a gate so we can A/B test whether bypassing the alt-screen +
viewport constraint lets the terminal's native scrollback beat our
virtualization on scroll perf.
Result: definitively NO. Inline mode is 40x worse on every metric
that moves, because AlternateScreen is what constrains the ScrollBox
to the viewport height. Without it, the ScrollBox grows to contain
every child of the transcript and every frame re-renders all 1100
messages.
Profile under hold-wheel_up (1106-msg session, 30Hz for 6s):
metric fullscreen inline delta
patches_total 28,864 1,111,574 +3751%
writeBytes_total 42 KB 1.6 MB +3881%
fps_throughput 15.8 fps 1.75 fps -89%
frames 179 18 -90%
gap_p50_ms 17 (~60fps) 726 (~1fps) +4170%
yoga_p99 34 ms 405 ms +1083%
renderer_p99 14 ms 169 ms +1062%
flickers 0 5 offscreen —
This is actually the cleanest data we've gotten so far:
* AlternateScreen is LOAD-BEARING for perf — its viewport height
constraint is what lets useVirtualHistory's culling work. No
constraint → ScrollBox grows unbounded → every fiber mounts.
* The outer terminal (Cursor's xterm.js) parsed 1.6 MB of ANSI in
under 10 seconds with drain p99 = 8.83 ms and 0 backpressure
frames. Our terminal-write hypothesis from last session was
wrong: the bottleneck is React + Yoga, not the wire.
* Doing proper inline mode (non-virtualized transcript in
scrollback, composer pinned below) is not a flag flip — it's a
different UI architecture. Leaving this flag in so anyone
re-running the experiment gets the same numbers, but not
building the architecture until we're sure the perf win is
worth the UX loss (it probably isn't — the fullscreen + virt
path is the one we should optimize, not replace).
Keeping the flag as an experiment gate. Flip HERMES_TUI_INLINE=1
and run scripts/profile-tui.py --compare to reproduce.
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
|
import { AlternateScreen, Box, NoSelect, ScrollBox, Text } from '@hermes/ink'
|
||||||
import { useStore } from '@nanostores/react'
|
import { useStore } from '@nanostores/react'
|
||||||
import { memo } from 'react'
|
import { Fragment, memo } from 'react'
|
||||||
|
|
||||||
import { useGateway } from '../app/gatewayContext.js'
|
import { useGateway } from '../app/gatewayContext.js'
|
||||||
import type { AppLayoutProps } from '../app/interfaces.js'
|
import type { AppLayoutProps } from '../app/interfaces.js'
|
||||||
import { $isBlocked, $overlayState, patchOverlayState } from '../app/overlayStore.js'
|
import { $isBlocked, $overlayState, patchOverlayState } from '../app/overlayStore.js'
|
||||||
import { $uiState } from '../app/uiStore.js'
|
import { $uiState } from '../app/uiStore.js'
|
||||||
|
import { INLINE_MODE } from '../config/env.js'
|
||||||
import { PLACEHOLDER } from '../content/placeholders.js'
|
import { PLACEHOLDER } from '../content/placeholders.js'
|
||||||
import { inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js'
|
import { inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js'
|
||||||
import { PerfPane } from '../lib/perfPane.js'
|
import { PerfPane } from '../lib/perfPane.js'
|
||||||
@@ -244,8 +245,23 @@ export const AppLayout = memo(function AppLayout({
|
|||||||
}: AppLayoutProps) {
|
}: AppLayoutProps) {
|
||||||
const overlay = useStore($overlayState)
|
const overlay = useStore($overlayState)
|
||||||
|
|
||||||
|
// Inline mode: skip <AlternateScreen> 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 (
|
return (
|
||||||
<AlternateScreen mouseTracking={mouseTracking}>
|
<Shell {...shellProps}>
|
||||||
<Box flexDirection="column" flexGrow={1}>
|
<Box flexDirection="column" flexGrow={1}>
|
||||||
<Box flexDirection="row" flexGrow={1}>
|
<Box flexDirection="row" flexGrow={1}>
|
||||||
{overlay.agents ? (
|
{overlay.agents ? (
|
||||||
@@ -277,6 +293,6 @@ export const AppLayout = memo(function AppLayout({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</AlternateScreen>
|
</Shell>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
export const STARTUP_RESUME_ID = (process.env.HERMES_TUI_RESUME ?? '').trim()
|
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 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())
|
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())
|
||||||
|
|||||||
Reference in New Issue
Block a user