hermes-agent/ui-tui/src/entry.tsx
laoli-no1 d33deb7cbe fix(tui): clear scrollback buffer on startup to prevent tmux scrollback leakage
When TUI exits, tmux captures some TUI output into its scrollback buffer.
On restart, stale scrollback content appears at the top of screen before
AlternateScreen takes over.

Add ANSI escape sequences at startup:
- ESC[2J  clear visible screen
- ESC[H   cursor home
- ESC[3J  clear scrollback buffer
2026-05-12 16:42:53 -07:00

88 lines
2.9 KiB
JavaScript

#!/usr/bin/env -S node --max-old-space-size=8192 --expose-gc
// Must be first import. If the user explicitly opts into truecolor, this
// nudges chalk / supports-color before either package is initialized.
import './lib/forceTruecolor.js'
import type { FrameEvent } from '@hermes/ink'
import { GatewayClient } from './gatewayClient.js'
import { setupGracefulExit } from './lib/gracefulExit.js'
import { formatBytes, type HeapDumpResult, performHeapDump } from './lib/memory.js'
import { type MemorySnapshot, startMemoryMonitor } from './lib/memoryMonitor.js'
import { resetTerminalModes } from './lib/terminalModes.js'
if (!process.stdin.isTTY) {
console.log('hermes-tui: no TTY')
process.exit(0)
}
// 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()
// Clear visible screen + scrollback buffer. Without this, tmux may retain
// stale TUI output in its scrollback buffer from the previous session,
// which is visible when the user scrolls up or briefly before AlternateScreen
// takes over on restart. See entry.tsx → AlternateScreen flow.
process.stdout.write('\x1b[2J\x1b[H\x1b[3J')
const gw = new GatewayClient()
gw.start()
const dumpNotice = (snap: MemorySnapshot, dump: HeapDumpResult | null) =>
`hermes-tui: ${snap.level} memory (${formatBytes(snap.heapUsed)}) — auto heap dump → ${dump?.heapPath ?? '(failed)'}\n`
setupGracefulExit({
cleanups: [
() => {
resetTerminalModes()
return gw.kill()
}
],
onError: (scope, err) => {
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
process.stderr.write(`hermes-tui ${scope}: ${message.slice(0, 2000)}\n`)
},
onSignal: signal => {
resetTerminalModes()
process.stderr.write(`hermes-tui: received ${signal}\n`)
}
})
const stopMemoryMonitor = startMemoryMonitor({
onCritical: (snap, dump) => {
resetTerminalModes()
process.stderr.write(dumpNotice(snap, dump))
process.stderr.write('hermes-tui: exiting to avoid OOM; restart to recover\n')
process.exit(137)
},
onHigh: (snap, dump) => process.stderr.write(dumpNotice(snap, dump))
})
if (process.env.HERMES_HEAPDUMP_ON_START === '1') {
void performHeapDump('manual')
}
process.on('beforeExit', () => stopMemoryMonitor())
const [ink, { App }, { logFrameEvent }, { trackFrame }] = await Promise.all([
import('@hermes/ink'),
import('./app.js'),
import('./lib/perfPane.js'),
import('./lib/fpsStore.js')
])
// Both consumers are undefined when their env flags are off; only attach
// onFrame when at least one is on so ink skips timing in the default case.
const onFrame =
logFrameEvent || trackFrame
? (event: FrameEvent) => {
logFrameEvent?.(event)
trackFrame?.(event.durationMs)
}
: undefined
ink.render(<App gw={gw} />, { exitOnCtrlC: false, onFrame })