fix(cli): clear screen on exit so live chrome isn't stranded in scrollback (#38928)

The classic CLI left its live bottom chrome — the status bar, input box,
and separator rules — frozen in terminal scrollback after exit, on every
exit path (/exit, /quit, Ctrl+C, EOF) and on both Linux and Windows. The
prior erase_when_done=True fix (bf82a7f1c) routes prompt_toolkit's teardown
through renderer.erase(), but that walks back by the renderer's internal
cursor model and does not reliably wipe the chrome in practice — users still
saw a dead status bar + the rest of the session sitting above the resume
summary.

Clear the screen + scrollback directly at the single exit funnel instead.
All exit paths converge on _print_exit_summary() (called from the run-loop
finally block after app.run() returns and prompt_toolkit has restored
terminal modes), so a new _clear_terminal_on_exit() helper runs there before
the summary prints. It writes ESC[3J ESC[2J ESC[H (erase scrollback, erase
screen, home cursor) on a real TTY, no-ops silently when stdout is not a
terminal (pipes/redirects), and falls back to the platform clear command if
the escape write fails. Works on Linux, macOS, and modern Windows terminals
(Terminal/conhost with VT processing, already enabled by prompt_toolkit).

The resume/goodbye summary now prints at a clean top-left with nothing
stranded above it.

Fixes #38252.
This commit is contained in:
Teknium 2026-06-04 04:38:35 -07:00 committed by GitHub
parent c0435f4fef
commit d3fab54933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

45
cli.py
View file

@ -12727,8 +12727,53 @@ class HermesCLI:
if tts_thread is not None and tts_thread.is_alive():
tts_thread.join(timeout=5)
def _clear_terminal_on_exit(self):
"""Clear screen + scrollback so nothing is stranded above the exit summary.
Called from ``_print_exit_summary`` after ``app.run()`` has returned and
prompt_toolkit has torn down its renderer + restored terminal modes
so a direct write to the real stdout fd is safe (the StdoutProxy /
patch_stdout layer is gone by now).
Sequence: ``ESC[3J`` (erase scrollback) + ``ESC[2J`` (erase visible
screen) + ``ESC[H`` (cursor home). Modern terminals on Linux, macOS and
Windows (Terminal / conhost with VT processing, which prompt_toolkit
already enables) all honor these. Best-effort: skip silently when
stdout isn't a real console, and fall back to the platform ``clear`` /
``cls`` command if the escape write fails.
"""
try:
stream = sys.stdout
if stream is None or not stream.isatty():
return
except Exception:
return
try:
stream.write("\033[3J\033[2J\033[H")
stream.flush()
return
except Exception:
pass
# Fallback: shell clear command (rarely needed — escapes work on every
# VT-capable terminal, but this covers exotic stdout wrappers).
try:
os.system("cls" if os.name == "nt" else "clear")
except Exception:
pass
def _print_exit_summary(self):
"""Print session resume info on exit, similar to Claude Code."""
# Clear the screen + scrollback before printing the summary so the
# live bottom chrome (status bar, input box, separator rules) and the
# rest of the session transcript don't get stranded above the exit
# summary (#38252). By this point app.run() has returned and
# prompt_toolkit has restored terminal modes, so writing raw escapes
# to stdout is safe. ESC[3J clears scrollback, ESC[2J clears the
# visible screen, ESC[H homes the cursor — so the summary prints at a
# clean top-left. Falls back to the platform clear command if stdout
# isn't a TTY-capable stream. Honors NO_COLOR/dumb terminals by
# skipping silently when there's no real console.
self._clear_terminal_on_exit()
print()
msg_count = len(self.conversation_history)
if msg_count > 0: