mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
fix(tui): install the process.on('exit') terminal-mode backstop (#42165)
#19194's fix added process.exit(0) to die()/dieWithCode() with a comment relying on a process.on('exit') handler in entry.tsx that resets terminal modes — but that handler was never installed. So /quit, Ctrl+C, Ctrl+D and every process.exit() path left DEC mouse tracking (?1000/1002/1003/1006) armed in the parent shell. The terminal then kept emitting mouse reports into stdin — read as keystrokes by the shell or a freshly relaunched TUI — surfacing as ...;...M garbage in the input box. Install the missing handler. 'exit' fires once on real termination and runs synchronous code only; resetTerminalModes() writes via writeSync, so the disable sequence lands before the process is gone. Fixes #28419
This commit is contained in:
parent
7230fcb7f2
commit
fd1e7c2bc3
2 changed files with 38 additions and 0 deletions
|
|
@ -36,4 +36,26 @@ describe('terminal mode reset', () => {
|
|||
expect(resetTerminalModes({ isTTY: false, write } as unknown as NodeJS.WriteStream)).toBe(false)
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// entry.tsx installs `process.on('exit', () => resetTerminalModes())` as the
|
||||
// final backstop (#28419): /quit, Ctrl+C, Ctrl+D and any process.exit() path
|
||||
// must disarm DEC mouse tracking so the parent shell / next TUI doesn't read
|
||||
// leaked mouse reports as keystrokes. 'exit' handlers run synchronously only,
|
||||
// so the reset must complete via a single synchronous write — verify that an
|
||||
// exit-style invocation disables every SGR mouse mode that produced the
|
||||
// reported `…;…M` garbage.
|
||||
it('disarms mouse tracking from a synchronous exit-style handler', () => {
|
||||
const write = vi.fn()
|
||||
const stream = { isTTY: true, write } as unknown as NodeJS.WriteStream
|
||||
|
||||
// Mirror entry.tsx's process.on('exit') callback.
|
||||
const onExit = () => resetTerminalModes(stream)
|
||||
onExit()
|
||||
|
||||
expect(write).toHaveBeenCalledTimes(1)
|
||||
const written = write.mock.calls[0]?.[0] as string
|
||||
for (const mode of ['\x1b[?1006l', '\x1b[?1003l', '\x1b[?1002l', '\x1b[?1000l']) {
|
||||
expect(written).toContain(mode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,6 +23,22 @@ if (!process.stdin.isTTY) {
|
|||
// terminal tab can still have mouse/focus/paste modes enabled.
|
||||
resetTerminalModes()
|
||||
|
||||
// Final backstop for terminal cleanup. setupGracefulExit() resets modes on
|
||||
// signals/uncaught errors, and die()/dieWithCode() call process.exit() after
|
||||
// Ink's unmount specifically so this handler can fire (see useMainApp.ts and
|
||||
// #19194). But that handler was never actually installed — so /quit, Ctrl+C,
|
||||
// Ctrl+D, and any process.exit() path left DEC mouse tracking (?1000/1002/
|
||||
// 1003/1006) armed in the parent shell. The terminal then keeps emitting mouse
|
||||
// reports into whatever reads stdin next — the shell or a freshly relaunched
|
||||
// TUI mid-init — which surface as `102;71M5;104;62M`-style garbage in the input
|
||||
// box (#28419). 'exit' fires exactly once on real termination and only runs
|
||||
// synchronous code; resetTerminalModes() writes via writeSync, so it completes
|
||||
// before the process is gone. Idempotent and cheap, so layering it under the
|
||||
// graceful-exit cleanups is safe.
|
||||
process.on('exit', () => {
|
||||
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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue