fix(cli): recover prompt_toolkit paint after tmux attach

Same-width SIGWINCH (typical tmux attach) skipped screen clear and
left previous_screen inconsistent, crashing redraw with
'cell' object has no attribute 'char'. Always clear on resize
recovery and retry _output_screen_diff with previous_screen=None
on AttributeError/TypeError.
This commit is contained in:
HunterSThompson
2026-08-11 11:42:07 +00:00
committed by Teknium
parent 88a1a9fd95
commit c3d7f6eefd
2 changed files with 146 additions and 14 deletions
+71 -14
View File
@@ -3614,6 +3614,58 @@ def _strip_leaked_bracketed_paste_wrappers(text: str) -> str:
return strip_leaked_bracketed_paste_wrappers(text)
def _hermes_call_output_screen_diff(
orig_osd,
app,
output,
screen,
current_pos,
color_depth,
previous_screen,
last_style,
is_done,
full_screen,
attrs_for_style_string,
style_string_has_style,
size,
previous_width,
):
"""Call prompt_toolkit ``_output_screen_diff`` with Hermes resize guards.
1. Inflate ``previous_screen.height`` when the new screen is taller so pt
skips the reserve-vertical-space cursor move that stamps chrome into
scrollback (pt #29 / Hermes #26137).
2. On AttributeError/TypeError from a corrupt previous paint buffer
(classic after tmux attach with same width), retry once with
``previous_screen=None`` so pt first-paints cleanly instead of crashing
the event loop with ``'cell' object has no attribute 'char'``.
"""
try:
if previous_screen is not None and hasattr(previous_screen, "height"):
if previous_screen.height < screen.height:
previous_screen.height = screen.height
except Exception:
pass
try:
return orig_osd(
app, output, screen, current_pos, color_depth,
previous_screen, last_style, is_done, full_screen,
attrs_for_style_string, style_string_has_style,
size, previous_width,
)
except (AttributeError, TypeError):
# Corrupt previous_screen / row cells after client reattach.
return orig_osd(
app, output, screen, current_pos, color_depth,
None, # previous_screen → first-paint erase path
None, # last_style
is_done, full_screen,
attrs_for_style_string, style_string_has_style,
size, 0, # previous_width → treat as changed
)
def _apply_bracketed_paste_timeout_patch() -> None:
"""Patch prompt_toolkit to recover from torn bracketed-paste sequences.
@@ -5162,9 +5214,14 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# reach, leaving a duplicated status bar stranded above the live origin.
# Ctrl+L / /redraw clears it cleanly, so route the resize path through
# the SAME recovery: wipe the visible viewport (banner-safe — CSI 2J
# only, never CSI 3J) and replay the transcript so nothing is lost.
# Row-count-only changes skip this (no reflow → no ghost) to avoid an
# unnecessary full repaint.
# by default; CSI 3J only when display.cli_rebuild_scrollback_on_redraw
# is enabled) and replay the transcript so nothing is lost.
# Same-width SIGWINCH (tmux attach, benign focus/tab signals) is left
# untouched — no clear, no replay — because a 2J without replay erases
# the visible transcript and a replay against preserved scrollback
# duplicates it (#65293). The stale-previous_screen crash tmux attach
# used to trigger is handled by _hermes_call_output_screen_diff's
# retry-with-first-paint instead (#83874).
try:
new_width = self._get_tui_terminal_width()
except Exception:
@@ -18110,20 +18167,20 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
other code path's behavior.
Critical: do NOT replace a None previous_screen with
a fresh Screen() — that would skip the proper
reset_attributes()+erase_down() at L178-185 which
fires when previous_screen is None (first-paint /
a fresh Screen() on the happy path — that would skip
the proper reset_attributes()+erase_down() at L178-185
which fires when previous_screen is None (first-paint /
width-change). Without that reset, ANSI styles
leak between renders.
"""
try:
if previous_screen is not None and hasattr(previous_screen, "height"):
if previous_screen.height < screen.height:
previous_screen.height = screen.height
except Exception:
pass
return _orig_osd(
Safety net: if the diff crashes with AttributeError /
TypeError (corrupt previous_screen after tmux attach —
"'cell' object has no attribute 'char'"), retry once
with previous_screen=None so pt takes the first-paint
erase path instead of wedging the event loop.
"""
return _hermes_call_output_screen_diff(
_orig_osd,
app, output, screen, current_pos, color_depth,
previous_screen, last_style, is_done, full_screen,
attrs_for_style_string, style_string_has_style,