mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
fix(tui): reset terminal modes on startup and exit
Reset sticky mouse/focus/paste terminal modes before the TUI starts and during graceful shutdown paths so stale tab state from prior crashes cannot poison the next session.
This commit is contained in:
parent
98a428fd61
commit
d05497f812
3 changed files with 87 additions and 2 deletions
29
ui-tui/src/__tests__/terminalModes.test.ts
Normal file
29
ui-tui/src/__tests__/terminalModes.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { resetTerminalModes, TERMINAL_MODE_RESET } from '../lib/terminalModes.js'
|
||||||
|
|
||||||
|
describe('terminal mode reset', () => {
|
||||||
|
it('includes the sticky input modes Hermes enables', () => {
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?1006l')
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?1003l')
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?1002l')
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?1000l')
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?1004l')
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?2004l')
|
||||||
|
expect(TERMINAL_MODE_RESET).toContain('\x1b[?1049l')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('writes reset sequence to TTY streams without fds', () => {
|
||||||
|
const write = vi.fn()
|
||||||
|
|
||||||
|
expect(resetTerminalModes({ isTTY: true, write } as unknown as NodeJS.WriteStream)).toBe(true)
|
||||||
|
expect(write).toHaveBeenCalledWith(TERMINAL_MODE_RESET)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips non-TTY streams', () => {
|
||||||
|
const write = vi.fn()
|
||||||
|
|
||||||
|
expect(resetTerminalModes({ isTTY: false, write } as unknown as NodeJS.WriteStream)).toBe(false)
|
||||||
|
expect(write).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -11,12 +11,17 @@ import { GatewayClient } from './gatewayClient.js'
|
||||||
import { setupGracefulExit } from './lib/gracefulExit.js'
|
import { setupGracefulExit } from './lib/gracefulExit.js'
|
||||||
import { formatBytes, type HeapDumpResult, performHeapDump } from './lib/memory.js'
|
import { formatBytes, type HeapDumpResult, performHeapDump } from './lib/memory.js'
|
||||||
import { type MemorySnapshot, startMemoryMonitor } from './lib/memoryMonitor.js'
|
import { type MemorySnapshot, startMemoryMonitor } from './lib/memoryMonitor.js'
|
||||||
|
import { resetTerminalModes } from './lib/terminalModes.js'
|
||||||
|
|
||||||
if (!process.stdin.isTTY) {
|
if (!process.stdin.isTTY) {
|
||||||
console.log('hermes-tui: no TTY')
|
console.log('hermes-tui: no TTY')
|
||||||
process.exit(0)
|
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()
|
||||||
|
|
||||||
const gw = new GatewayClient()
|
const gw = new GatewayClient()
|
||||||
|
|
||||||
gw.start()
|
gw.start()
|
||||||
|
|
@ -25,17 +30,27 @@ const dumpNotice = (snap: MemorySnapshot, dump: HeapDumpResult | null) =>
|
||||||
`hermes-tui: ${snap.level} memory (${formatBytes(snap.heapUsed)}) — auto heap dump → ${dump?.heapPath ?? '(failed)'}\n`
|
`hermes-tui: ${snap.level} memory (${formatBytes(snap.heapUsed)}) — auto heap dump → ${dump?.heapPath ?? '(failed)'}\n`
|
||||||
|
|
||||||
setupGracefulExit({
|
setupGracefulExit({
|
||||||
cleanups: [() => gw.kill()],
|
cleanups: [
|
||||||
|
() => {
|
||||||
|
resetTerminalModes()
|
||||||
|
|
||||||
|
return gw.kill()
|
||||||
|
}
|
||||||
|
],
|
||||||
onError: (scope, err) => {
|
onError: (scope, err) => {
|
||||||
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
const message = err instanceof Error ? `${err.name}: ${err.message}` : String(err)
|
||||||
|
|
||||||
process.stderr.write(`hermes-tui ${scope}: ${message.slice(0, 2000)}\n`)
|
process.stderr.write(`hermes-tui ${scope}: ${message.slice(0, 2000)}\n`)
|
||||||
},
|
},
|
||||||
onSignal: signal => process.stderr.write(`hermes-tui: received ${signal}\n`)
|
onSignal: signal => {
|
||||||
|
resetTerminalModes()
|
||||||
|
process.stderr.write(`hermes-tui: received ${signal}\n`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const stopMemoryMonitor = startMemoryMonitor({
|
const stopMemoryMonitor = startMemoryMonitor({
|
||||||
onCritical: (snap, dump) => {
|
onCritical: (snap, dump) => {
|
||||||
|
resetTerminalModes()
|
||||||
process.stderr.write(dumpNotice(snap, dump))
|
process.stderr.write(dumpNotice(snap, dump))
|
||||||
process.stderr.write('hermes-tui: exiting to avoid OOM; restart to recover\n')
|
process.stderr.write('hermes-tui: exiting to avoid OOM; restart to recover\n')
|
||||||
process.exit(137)
|
process.exit(137)
|
||||||
|
|
|
||||||
41
ui-tui/src/lib/terminalModes.ts
Normal file
41
ui-tui/src/lib/terminalModes.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { writeSync } from 'node:fs'
|
||||||
|
|
||||||
|
export const TERMINAL_MODE_RESET =
|
||||||
|
'\x1b[?1006l' + // SGR mouse
|
||||||
|
'\x1b[?1003l' + // any-motion mouse
|
||||||
|
'\x1b[?1002l' + // button-motion mouse
|
||||||
|
'\x1b[?1000l' + // click mouse
|
||||||
|
'\x1b[?1004l' + // focus events
|
||||||
|
'\x1b[?2004l' + // bracketed paste
|
||||||
|
'\x1b[?1049l' + // alternate screen
|
||||||
|
'\x1b[0m' + // attributes
|
||||||
|
'\x1b[?25h' // cursor visible
|
||||||
|
|
||||||
|
type ResettableStream = Pick<NodeJS.WriteStream, 'isTTY' | 'write'> & {
|
||||||
|
fd?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetTerminalModes(stream: ResettableStream = process.stdout): boolean {
|
||||||
|
if (!stream.isTTY) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const fd = typeof stream.fd === 'number' ? stream.fd : stream === process.stdout ? 1 : undefined
|
||||||
|
if (fd !== undefined) {
|
||||||
|
try {
|
||||||
|
writeSync(fd, TERMINAL_MODE_RESET)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
// Fall through to stream.write for mocked or unusual TTY streams.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stream.write(TERMINAL_MODE_RESET)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue