Merge pull request #19307 from NousResearch/bb/fix-terminal-resize-jumble
fix(tui): clear Apple Terminal resize artifacts
This commit is contained in:
@@ -73,7 +73,13 @@ import {
|
|||||||
startSelection,
|
startSelection,
|
||||||
updateSelection
|
updateSelection
|
||||||
} from './selection.js'
|
} from './selection.js'
|
||||||
import { supportsExtendedKeys, SYNC_OUTPUT_SUPPORTED, type Terminal, writeDiffToTerminal } from './terminal.js'
|
import {
|
||||||
|
needsAltScreenResizeScrollbackClear,
|
||||||
|
supportsExtendedKeys,
|
||||||
|
SYNC_OUTPUT_SUPPORTED,
|
||||||
|
type Terminal,
|
||||||
|
writeDiffToTerminal
|
||||||
|
} from './terminal.js'
|
||||||
import {
|
import {
|
||||||
CURSOR_HOME,
|
CURSOR_HOME,
|
||||||
cursorMove,
|
cursorMove,
|
||||||
@@ -82,7 +88,8 @@ import {
|
|||||||
DISABLE_MODIFY_OTHER_KEYS,
|
DISABLE_MODIFY_OTHER_KEYS,
|
||||||
ENABLE_KITTY_KEYBOARD,
|
ENABLE_KITTY_KEYBOARD,
|
||||||
ENABLE_MODIFY_OTHER_KEYS,
|
ENABLE_MODIFY_OTHER_KEYS,
|
||||||
ERASE_SCREEN
|
ERASE_SCREEN,
|
||||||
|
ERASE_SCROLLBACK
|
||||||
} from './termio/csi.js'
|
} from './termio/csi.js'
|
||||||
import {
|
import {
|
||||||
DBP,
|
DBP,
|
||||||
@@ -121,6 +128,11 @@ const ERASE_THEN_HOME_PATCH = Object.freeze({
|
|||||||
content: ERASE_SCREEN + CURSOR_HOME
|
content: ERASE_SCREEN + CURSOR_HOME
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const DEEP_ERASE_THEN_HOME_PATCH = Object.freeze({
|
||||||
|
type: 'stdout' as const,
|
||||||
|
content: ERASE_SCREEN + ERASE_SCROLLBACK + CURSOR_HOME
|
||||||
|
})
|
||||||
|
|
||||||
// Cached per-Ink-instance, invalidated on resize. frame.cursor.y for
|
// Cached per-Ink-instance, invalidated on resize. frame.cursor.y for
|
||||||
// alt-screen is always terminalRows - 1 (renderer.ts).
|
// alt-screen is always terminalRows - 1 (renderer.ts).
|
||||||
function makeAltScreenParkPatch(terminalRows: number) {
|
function makeAltScreenParkPatch(terminalRows: number) {
|
||||||
@@ -863,17 +875,17 @@ export default class Ink {
|
|||||||
// position independently. Parking at bottom (not 0,0) keeps the guide
|
// position independently. Parking at bottom (not 0,0) keeps the guide
|
||||||
// where the user's attention is.
|
// where the user's attention is.
|
||||||
//
|
//
|
||||||
// After resize, prepend ERASE_SCREEN too. The diff only writes cells
|
// After resize, prepend a clear too. The diff only writes cells
|
||||||
// that changed; cells where new=blank and prev-buffer=blank get skipped
|
// that changed; cells where new=blank and prev-buffer=blank get skipped
|
||||||
// — but the physical terminal still has stale content there (shorter
|
// — but the physical terminal still has stale content there (shorter
|
||||||
// lines at new width leave old-width text tails visible). ERASE inside
|
// lines at new width leave old-width text tails visible). Apple Terminal
|
||||||
// BSU/ESU is atomic: old content stays visible until the whole
|
// can also preserve alt-screen reflow artifacts in scrollback during
|
||||||
// erase+paint lands, then swaps in one go. Writing ERASE_SCREEN
|
// resize, so it gets CSI 3J in this one recovery path. When BSU/ESU is
|
||||||
// synchronously in handleResize would blank the screen for the ~80ms
|
// supported, the clear+paint lands atomically; otherwise the final state
|
||||||
// render() takes.
|
// is still healed even if the repaint is visible.
|
||||||
if (this.needsEraseBeforePaint) {
|
if (this.needsEraseBeforePaint) {
|
||||||
this.needsEraseBeforePaint = false
|
this.needsEraseBeforePaint = false
|
||||||
optimized.unshift(ERASE_THEN_HOME_PATCH)
|
optimized.unshift(needsAltScreenResizeScrollbackClear() ? DEEP_ERASE_THEN_HOME_PATCH : ERASE_THEN_HOME_PATCH)
|
||||||
} else {
|
} else {
|
||||||
optimized.unshift(CURSOR_HOME_PATCH)
|
optimized.unshift(CURSOR_HOME_PATCH)
|
||||||
}
|
}
|
||||||
|
|||||||
15
ui-tui/packages/hermes-ink/src/ink/terminal.test.ts
Normal file
15
ui-tui/packages/hermes-ink/src/ink/terminal.test.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { needsAltScreenResizeScrollbackClear } from './terminal.js'
|
||||||
|
|
||||||
|
describe('terminal resize quirks', () => {
|
||||||
|
it('uses a deeper alt-screen resize clear for Apple Terminal', () => {
|
||||||
|
expect(needsAltScreenResizeScrollbackClear({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(true)
|
||||||
|
expect(needsAltScreenResizeScrollbackClear({ TERM_PROGRAM: ' Apple_Terminal ' })).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the normal resize repaint path for modern terminals', () => {
|
||||||
|
expect(needsAltScreenResizeScrollbackClear({ TERM_PROGRAM: 'vscode' })).toBe(false)
|
||||||
|
expect(needsAltScreenResizeScrollbackClear({ TERM_PROGRAM: 'iTerm.app' })).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -168,6 +168,10 @@ export function isXtermJs(): boolean {
|
|||||||
return xtversionName?.startsWith('xterm.js') ?? false
|
return xtversionName?.startsWith('xterm.js') ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function needsAltScreenResizeScrollbackClear(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||||
|
return (env.TERM_PROGRAM ?? '').trim() === 'Apple_Terminal'
|
||||||
|
}
|
||||||
|
|
||||||
// Terminals known to correctly implement the Kitty keyboard protocol
|
// Terminals known to correctly implement the Kitty keyboard protocol
|
||||||
// (CSI >1u) and/or xterm modifyOtherKeys (CSI >4;2m) for ctrl+shift+<letter>
|
// (CSI >1u) and/or xterm modifyOtherKeys (CSI >4;2m) for ctrl+shift+<letter>
|
||||||
// disambiguation. We previously enabled unconditionally (#23350), assuming
|
// disambiguation. We previously enabled unconditionally (#23350), assuming
|
||||||
|
|||||||
Reference in New Issue
Block a user