fix(cli): disable prompt_toolkit CPR queries to stop escape-sequence leak (#13870)

prompt_toolkit's renderer sends ESC[6n cursor-position queries before
painting in non-fullscreen mode; the terminal replies ESC[<row>;<col>R.
Over SSH/cloudflared tunnels and slow PTYs these replies race past the
input parser and land in the display as raw '20;1R21;1R' text, and the
pending-CPR future can stall the renderer so the prompt freezes after the
agent's final answer.

Build the prompt_toolkit output with enable_cpr=False so CPR is marked
NOT_SUPPORTED up front and ESC[6n is never sent. This is the root-cause
counterpart to the existing input-side _strip_leaked_terminal_responses
scrubbing. Vt100_Output.from_pty() does not expose enable_cpr in
prompt_toolkit 3.x, so _build_cpr_disabled_output() reproduces its
get_size setup and calls the constructor directly; it returns None on any
failure so startup falls back to the default output.

Verified in a real PTY: baseline emits 1 ESC[6n query, the fix emits 0,
banner/UI render identically. Layout is unaffected — with CPR off the
renderer sizes the prompt to its preferred height (the same fallback
prompt_toolkit uses on any terminal that doesn't answer CPR).

Co-authored-by: Hermes Agent <noreply@nousresearch.com>
This commit is contained in:
HiddenPuppy 2026-06-27 03:52:26 -07:00 committed by Teknium
parent e7c013494d
commit b34771fc06
2 changed files with 157 additions and 1 deletions

View file

@ -247,6 +247,73 @@ class TestPromptToolkitTerminalCompatibility:
assert renderer.cpr_not_supported_callback is None
def test_cpr_disabled_output_marks_renderer_not_supported(self):
"""CPR-disabled output must make prompt_toolkit skip ESC[6n entirely.
The root cause of #13870 is that prompt_toolkit sends ESC[6n cursor
queries whose CPR replies leak into the display over tunnels/slow PTYs.
Building the output with enable_cpr=False is what stops the queries:
the renderer marks CPR NOT_SUPPORTED and never calls ask_for_cpr().
"""
import sys as _sys
from cli import _build_cpr_disabled_output
from prompt_toolkit.application import Application
from prompt_toolkit.layout import Layout, Window, FormattedTextControl
from prompt_toolkit.renderer import CPR_Support
out = _build_cpr_disabled_output(_sys.stdout)
assert out is not None
# The contract: this output does not respond to CPR.
assert out.enable_cpr is False
assert out.responds_to_cpr is False
# And wired into an Application, the renderer treats CPR as unsupported,
# so request_absolute_cursor_position() never sends ESC[6n.
app = Application(
layout=Layout(Window(FormattedTextControl("x"))),
output=out,
full_screen=False,
)
assert app.renderer.cpr_support == CPR_Support.NOT_SUPPORTED
def test_cpr_disabled_output_returns_none_on_failure(self):
"""A non-fileno stdout must degrade to None (default output fallback)."""
from cli import _build_cpr_disabled_output
class _NoFileno:
def fileno(self):
raise OSError("not a real fd")
# Build must not raise; worst case it returns a usable output or None.
# The hard guarantee is no exception escapes (startup must never break).
result = _build_cpr_disabled_output(_NoFileno())
assert result is None or result.enable_cpr is False
def test_cpr_gating_local_vs_tunnel(self, monkeypatch):
"""CPR is only suppressed on tunneled links / explicit opt-out.
CPR works fine on local terminals and is only a layout hint, so the fix
for #13870 must not change default behavior locally — it gates on
_terminal_may_leak_cpr(). Local (no SSH env) -> CPR left enabled;
SSH session or PROMPT_TOOLKIT_NO_CPR=1 -> CPR suppressed.
"""
from cli import _terminal_may_leak_cpr
for var in ("SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY", "PROMPT_TOOLKIT_NO_CPR"):
monkeypatch.delenv(var, raising=False)
# Local terminal: leave prompt_toolkit's default (CPR on) untouched.
assert _terminal_may_leak_cpr() is False
# SSH session: the tunnel where the leak reproduces.
monkeypatch.setenv("SSH_CONNECTION", "10.0.0.1 22 10.0.0.2 51234")
assert _terminal_may_leak_cpr() is True
monkeypatch.delenv("SSH_CONNECTION", raising=False)
# prompt_toolkit's own explicit opt-out is honored.
monkeypatch.setenv("PROMPT_TOOLKIT_NO_CPR", "1")
assert _terminal_may_leak_cpr() is True
class TestSingleQueryState:
def test_voice_and_interrupt_state_initialized_before_run(self):