mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-28 06:21:33 +00:00
fix(ui-tui): heal same-dimension alt-screen resize drift
- Treat same-dimension resize events in alt-screen mode as a repaint signal, because terminal hosts can reflow or restore the physical buffer without changing columns/rows. - Ensure pending resize erases are emitted even when the virtual diff is empty, so stale physical glyphs are still cleared. - Extract alt-screen resize repaint into prepareAltScreenResizeRepaint() for readability. - Add defensive clearTimeout in prepareAltScreenResizeRepaint so rapid resize bursts don't stack redundant delayed repaints. - Add a focused regression test for same-dimension alt-screen resize healing. Addresses #18449 Related to #17961
This commit is contained in:
parent
2844c888f1
commit
4813aaf0ba
2 changed files with 97 additions and 30 deletions
|
|
@ -484,17 +484,22 @@ export default class Ink {
|
|||
private handleResize = () => {
|
||||
const cols = this.options.stdout.columns || 80
|
||||
const rows = this.options.stdout.rows || 24
|
||||
const dimsChanged = cols !== this.terminalColumns || rows !== this.terminalRows
|
||||
|
||||
// Terminals often emit 2+ resize events for one user action (window
|
||||
// settling). Same-dimension events are no-ops; skip to avoid redundant
|
||||
// frame resets and renders.
|
||||
if (cols === this.terminalColumns && rows === this.terminalRows) {
|
||||
// Terminals often emit 2+ resize events for one user action
|
||||
// (window settling). Same-dimension events are usually no-ops,
|
||||
// but in alt-screen mode a same-dimension resize can signal a
|
||||
// terminal host reflow or buffer restore that leaves stale glyphs
|
||||
// on the physical screen — treat it as a repaint signal.
|
||||
if (!dimsChanged && !(this.altScreenActive && !this.isPaused && this.options.stdout.isTTY)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.terminalColumns = cols
|
||||
this.terminalRows = rows
|
||||
this.altScreenParkPatch = makeAltScreenParkPatch(this.terminalRows)
|
||||
if (dimsChanged) {
|
||||
this.terminalColumns = cols
|
||||
this.terminalRows = rows
|
||||
this.altScreenParkPatch = makeAltScreenParkPatch(this.terminalRows)
|
||||
}
|
||||
|
||||
// Pending throttled/drain work captured stale dims — cancel so
|
||||
// the upcoming microtask owns the next frame.
|
||||
|
|
@ -521,26 +526,7 @@ export default class Ink {
|
|||
// doesn't exit alt-screen. Do NOT write ERASE_SCREEN: render() below
|
||||
// can take ~80ms; erasing first leaves the screen blank that whole time.
|
||||
if (this.altScreenActive && !this.isPaused && this.options.stdout.isTTY) {
|
||||
if (this.altScreenMouseTracking) {
|
||||
this.options.stdout.write(ENABLE_MOUSE_TRACKING)
|
||||
}
|
||||
|
||||
this.resetFramesForAltScreen()
|
||||
this.needsEraseBeforePaint = true
|
||||
|
||||
// One last repaint after the resize burst settles closes any host-side
|
||||
// reflow drift the normal diff path can't see.
|
||||
this.resizeSettleTimer = setTimeout(() => {
|
||||
this.resizeSettleTimer = null
|
||||
|
||||
if (!this.canAltScreenRepaint()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.resetFramesForAltScreen()
|
||||
this.needsEraseBeforePaint = true
|
||||
this.render(this.currentNode!)
|
||||
}, 160)
|
||||
this.prepareAltScreenResizeRepaint()
|
||||
}
|
||||
|
||||
// Already queued: later events in this burst updated dims/alt-screen
|
||||
|
|
@ -573,6 +559,36 @@ export default class Ink {
|
|||
)
|
||||
}
|
||||
|
||||
private prepareAltScreenResizeRepaint(): void {
|
||||
// Clear any pending settle timer from a previous resize burst so
|
||||
// rapid events don't stack redundant delayed repaints. (handleResize
|
||||
// also clears this, but the defensive clear keeps the method safe
|
||||
// if it's ever called from other code paths.)
|
||||
if (this.resizeSettleTimer !== null) {
|
||||
clearTimeout(this.resizeSettleTimer)
|
||||
this.resizeSettleTimer = null
|
||||
}
|
||||
|
||||
if (this.altScreenMouseTracking) {
|
||||
this.options.stdout.write(ENABLE_MOUSE_TRACKING)
|
||||
}
|
||||
|
||||
this.resetFramesForAltScreen()
|
||||
this.needsEraseBeforePaint = true
|
||||
|
||||
this.resizeSettleTimer = setTimeout(() => {
|
||||
this.resizeSettleTimer = null
|
||||
|
||||
if (!this.canAltScreenRepaint()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.resetFramesForAltScreen()
|
||||
this.needsEraseBeforePaint = true
|
||||
this.render(this.currentNode!)
|
||||
}, 160)
|
||||
}
|
||||
|
||||
resolveExitPromise: () => void = () => {}
|
||||
rejectExitPromise: (reason?: Error) => void = () => {}
|
||||
unsubscribeExit: () => void = () => {}
|
||||
|
|
@ -919,8 +935,9 @@ export default class Ink {
|
|||
const optimized = optimize(diff)
|
||||
const optimizeMs = performance.now() - tOptimize
|
||||
const hasDiff = optimized.length > 0
|
||||
const needsAltScreenErase = this.altScreenActive && this.needsEraseBeforePaint
|
||||
|
||||
if (this.altScreenActive && hasDiff) {
|
||||
if (this.altScreenActive && (hasDiff || needsAltScreenErase)) {
|
||||
// Prepend CSI H to anchor the physical cursor to (0,0) so
|
||||
// log-update's relative moves compute from a known spot (self-healing
|
||||
// against out-of-band cursor drift, see the ALT_SCREEN_ANCHOR_CURSOR
|
||||
|
|
@ -940,7 +957,7 @@ export default class Ink {
|
|||
// resize, so it gets CSI 3J in this one recovery path. When BSU/ESU is
|
||||
// supported, the clear+paint lands atomically; otherwise the final state
|
||||
// is still healed even if the repaint is visible.
|
||||
if (this.needsEraseBeforePaint) {
|
||||
if (needsAltScreenErase) {
|
||||
this.needsEraseBeforePaint = false
|
||||
optimized.unshift(needsAltScreenResizeScrollbackClear() ? DEEP_ERASE_THEN_HOME_PATCH : ERASE_THEN_HOME_PATCH)
|
||||
} else {
|
||||
|
|
@ -1062,7 +1079,7 @@ export default class Ink {
|
|||
this.lastDrainMs = 0
|
||||
|
||||
// Only track drain on TTY. Piped/non-TTY stdout bypasses flow control.
|
||||
const trackDrain = this.options.stdout.isTTY && hasDiff
|
||||
const trackDrain = this.options.stdout.isTTY && optimized.length > 0
|
||||
const drainStart = trackDrain ? tWrite : 0
|
||||
|
||||
if (trackDrain) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue