The lingering characters are an emoji-width desync, and the earlier fix aimed
at the wrong glyph and the wrong screens.
prompt_toolkit paints differentially with a *relative* cursor, advancing by its
own width model. That is sound only while every glyph is one cell. An emoji is
width-2 in both Rich and pt, but the terminal draws it in a single cell, so from
the emoji onward the row's cursor model is one column off. On the next paint, if
the emoji itself didn't change it is skipped, and any cell pt repositions to on
its right lands a column early — stranding the stale cell it meant to overwrite.
That is the lingering, and it needs an emoji on the line (confirmed: 👋).
The node-type marks (▲●■◉○★) and chart braille the previous attempt chased are
width-1 in Rich, pt, and the terminal — they never desync. That is why scoping
the repaint to floating route-graph dialogs did nothing: the real trigger is
emoji, which live in chat content and menu icons on ordinary full-frame screens.
Fix the invariant instead of the screen list: differential paint is only sound
when every glyph is one cell, so when a composed frame carries a width-2 glyph,
drop pt's cached frame (_emit → _invalidate_last_frame) and let the next paint be
a full erase_down + redraw — every cell rewritten contiguously, the terminal's
own cursor advance keeping the row aligned, nothing stale left behind. Checked at
compose time, so it covers a scroll, resize, or timer tick, on any screen. Pure
width-1 frames keep the fast differential paint. Drops the _SCROLL_ACTIONS /
_has_float dispatch hook the float-only theory needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>