fix(cli): let redraw recovery rebuild scrollback
This commit is contained in:
@@ -501,6 +501,12 @@ def load_cli_config() -> Dict[str, Any]:
|
||||
"busy_input_mode": "interrupt",
|
||||
"persistent_output": True,
|
||||
"persistent_output_max_lines": 200,
|
||||
# Clear terminal scrollback as well as the visible viewport when the
|
||||
# classic CLI performs a full redraw/resize recovery. Disabled by
|
||||
# default because some users prefer preserving terminal history;
|
||||
# enable when a terminal/tmux stack stamps stale prompt chrome into
|
||||
# scrollback during fullscreen/restore resizes.
|
||||
"cli_rebuild_scrollback_on_redraw": False,
|
||||
# Print a one-line summary of resolved modal prompts (approval /
|
||||
# clarify) into scrollback so the decision survives the repaint.
|
||||
"persist_prompts": True,
|
||||
@@ -5013,13 +5019,34 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
app = getattr(self, "_app", None)
|
||||
if not app:
|
||||
return
|
||||
self._clear_prompt_toolkit_screen(app)
|
||||
self._clear_prompt_toolkit_screen(
|
||||
app,
|
||||
rebuild_scrollback=self._redraw_rebuilds_scrollback(),
|
||||
)
|
||||
_replay_output_history()
|
||||
try:
|
||||
app.invalidate()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _redraw_rebuilds_scrollback() -> bool:
|
||||
"""Return whether CLI redraw/resize recovery should clear scrollback.
|
||||
|
||||
Some terminal/tmux stacks move prompt_toolkit's non-fullscreen bottom
|
||||
chrome into scrollback when the window is maximized/restored. A normal
|
||||
CSI 2J viewport clear cannot remove those stale prompt/input-rule rows,
|
||||
so users who hit that class of bug need CSI 3J as well, followed by the
|
||||
existing bounded output-history replay.
|
||||
"""
|
||||
display_config = CLI_CONFIG.get("display") if isinstance(CLI_CONFIG, dict) else {}
|
||||
if not isinstance(display_config, dict):
|
||||
display_config = {}
|
||||
raw = display_config.get("cli_rebuild_scrollback_on_redraw", False)
|
||||
if isinstance(raw, str):
|
||||
return raw.strip().lower() in {"1", "true", "yes", "on", "always"}
|
||||
return bool(raw)
|
||||
|
||||
def _recover_terminal_after_interrupt(self) -> None:
|
||||
"""Recover the terminal after an interrupted agent turn (#33271).
|
||||
|
||||
@@ -5160,7 +5187,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
)
|
||||
if width_changed:
|
||||
try:
|
||||
self._clear_prompt_toolkit_screen(app, rebuild_scrollback=False)
|
||||
self._clear_prompt_toolkit_screen(
|
||||
app,
|
||||
rebuild_scrollback=self._redraw_rebuilds_scrollback(),
|
||||
)
|
||||
_replay_output_history()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -1214,6 +1214,12 @@ DEFAULT_CONFIG = {
|
||||
# behaves badly with replayed scrollback.
|
||||
"persistent_output": True,
|
||||
"persistent_output_max_lines": 200,
|
||||
# Clear terminal scrollback as well as the visible viewport when the
|
||||
# classic CLI performs a full redraw/resize recovery. Disabled by
|
||||
# default because some users prefer preserving terminal history;
|
||||
# enable when a terminal/tmux stack stamps stale prompt chrome into
|
||||
# scrollback during fullscreen/restore window transitions.
|
||||
"cli_rebuild_scrollback_on_redraw": False,
|
||||
# Print a one-line summary of resolved modal prompts (approval /
|
||||
# clarify) into scrollback so the question and decision survive the
|
||||
# panel repaint. Set false to keep scrollback untouched.
|
||||
|
||||
@@ -53,6 +53,11 @@ class TestForceFullRedraw:
|
||||
monkeypatch.setattr(bare_cli, "_get_tui_terminal_width", lambda: 90)
|
||||
monkeypatch.setattr(bare_cli, "_schedule_status_bar_unsuppress", lambda *_: None)
|
||||
monkeypatch.setattr(cli_mod, "_replay_output_history", lambda: events.append("replay"))
|
||||
monkeypatch.setattr(
|
||||
cli_mod,
|
||||
"CLI_CONFIG",
|
||||
{"display": {"cli_rebuild_scrollback_on_redraw": False}},
|
||||
)
|
||||
|
||||
bare_cli._recover_after_resize(app, original_on_resize)
|
||||
|
||||
@@ -66,6 +71,57 @@ class TestForceFullRedraw:
|
||||
assert bare_cli._last_resize_width == 90
|
||||
assert bare_cli._status_bar_suppressed_after_resize is True
|
||||
|
||||
def test_force_redraw_uses_full_screen_clear_without_scrollback_clear(self, bare_cli, monkeypatch):
|
||||
app = MagicMock()
|
||||
bare_cli._app = app
|
||||
monkeypatch.setattr(
|
||||
cli_mod,
|
||||
"CLI_CONFIG",
|
||||
{"display": {"cli_rebuild_scrollback_on_redraw": False}},
|
||||
)
|
||||
|
||||
bare_cli._force_full_redraw()
|
||||
|
||||
app.renderer.output.erase_screen.assert_called_once()
|
||||
app.renderer.output.cursor_goto.assert_called_once_with(0, 0)
|
||||
app.renderer.output.write_raw.assert_not_called()
|
||||
|
||||
def test_force_redraw_can_clear_scrollback_when_configured(self, bare_cli, monkeypatch):
|
||||
app = MagicMock()
|
||||
bare_cli._app = app
|
||||
monkeypatch.setattr(
|
||||
cli_mod,
|
||||
"CLI_CONFIG",
|
||||
{"display": {"cli_rebuild_scrollback_on_redraw": True}},
|
||||
)
|
||||
|
||||
bare_cli._force_full_redraw()
|
||||
|
||||
app.renderer.output.erase_screen.assert_called_once()
|
||||
app.renderer.output.write_raw.assert_called_once_with("\x1b[3J")
|
||||
|
||||
def test_resize_recovery_can_clear_scrollback_when_configured(self, bare_cli, monkeypatch):
|
||||
app = MagicMock()
|
||||
events = []
|
||||
app.renderer.output.erase_screen.side_effect = lambda: events.append("erase")
|
||||
app.renderer.output.write_raw.side_effect = lambda *_: events.append("scrollback_wipe")
|
||||
original_on_resize = lambda: events.append("original_resize")
|
||||
|
||||
bare_cli._status_bar_suppressed_after_resize = False
|
||||
bare_cli._last_resize_width = 200
|
||||
monkeypatch.setattr(bare_cli, "_get_tui_terminal_width", lambda: 90)
|
||||
monkeypatch.setattr(bare_cli, "_schedule_status_bar_unsuppress", lambda *_: None)
|
||||
monkeypatch.setattr(cli_mod, "_replay_output_history", lambda: events.append("replay"))
|
||||
monkeypatch.setattr(
|
||||
cli_mod,
|
||||
"CLI_CONFIG",
|
||||
{"display": {"cli_rebuild_scrollback_on_redraw": "true"}},
|
||||
)
|
||||
|
||||
bare_cli._recover_after_resize(app, original_on_resize)
|
||||
|
||||
assert events[:3] == ["erase", "scrollback_wipe", "replay"]
|
||||
assert events.index("scrollback_wipe") < events.index("original_resize")
|
||||
|
||||
def test_resize_recovery_is_debounced(self, bare_cli, monkeypatch):
|
||||
timers = []
|
||||
|
||||
Reference in New Issue
Block a user