hermes-agent/ui-tui/src/entry.tsx
adybag14-cyber 7c2ff742a4 fix(tui): termux-gate scrollback preservation, touch-friendly defaults
Adds a Termux runtime detection helper and gates three TUI defaults on it:

- Skip the startup scrollback clear on Termux so users can review/copy
  earlier output after reopening the app. Desktop keeps the existing
  \x1b[2J\x1b[H\x1b[3J slate (AlternateScreen takes over there anyway).
- Default INLINE_MODE on under Termux: primary-buffer rendering makes
  long-thread review and copy/paste much less fragile when users
  background/foreground the app. Override with HERMES_TUI_INLINE=0/1.
- Default mouse tracking off under Termux so touch selection isn't
  intercepted by terminal mouse protocols. Explicit override via
  HERMES_TUI_MOUSE_TRACKING=0/1; legacy HERMES_TUI_DISABLE_MOUSE still
  works on desktop.

Detection is purely env-based (TERMUX_VERSION or PREFIX path) with an
explicit opt-out HERMES_TUI_TERMUX_MODE=0 for debugging. Non-Termux
platforms keep every existing default.

Co-authored-by: adybag14-cyber <252811164+adybag14-cyber@users.noreply.github.com>
2026-05-19 12:49:23 -07:00

103 lines
3.3 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 { TERMUX_TUI_MODE } from './config/env.js'
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 { openExternalUrl } from './lib/openExternalUrl.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()
// Desktop terminals benefit from a clean startup slate because the TUI usually
// runs in AlternateScreen. On Termux we keep prior output intact so users can
// review/copy earlier assistant replies after reopening the app.
if (TERMUX_TUI_MODE) {
process.stdout.write('\n')
} else {
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,
// Open URLs in the user's default browser when a link cell is clicked.
// The TUI's mouse tracking captures click events before Terminal.app's
// own URL detection can fire, so without this hook clicks on `<Link>`
// do nothing in any terminal where mouseTracking is on.
onHyperlinkClick: url => {
openExternalUrl(url)
}
})