From d3fab54933c3866d2c7cf5e51dc63e9e494c9f47 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 4 Jun 2026 04:38:35 -0700 Subject: [PATCH] fix(cli): clear screen on exit so live chrome isn't stranded in scrollback (#38928) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cli.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/cli.py b/cli.py index 46167a774a1..03ed1df00c5 100644 --- a/cli.py +++ b/cli.py @@ -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: