mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +00:00
fix(tui): keep DECSTBM scroll region off bottom row (#26683)
Avoid shifting the terminal's last visible row in the alt-screen DECSTBM fast path, which can leave transient scroll bleed/discoloration artifacts around the status lane until a repaint. Add regression tests to preserve the fast path when safe and skip it when the hint touches the bottom row.
This commit is contained in:
parent
6784c80794
commit
566d8f0d75
2 changed files with 46 additions and 1 deletions
|
|
@ -42,6 +42,8 @@ const stdoutOnly = (diff: ReturnType<LogUpdate['render']>) =>
|
||||||
.map(p => (p as { type: 'stdout'; content: string }).content)
|
.map(p => (p as { type: 'stdout'; content: string }).content)
|
||||||
.join('')
|
.join('')
|
||||||
|
|
||||||
|
const hasDecstbm = (text: string) => /\x1b\[\d+;\d+r/.test(text)
|
||||||
|
|
||||||
describe('LogUpdate.render diff contract', () => {
|
describe('LogUpdate.render diff contract', () => {
|
||||||
it('emits only changed cells when most rows match', () => {
|
it('emits only changed cells when most rows match', () => {
|
||||||
const w = 20
|
const w = 20
|
||||||
|
|
@ -154,4 +156,44 @@ describe('LogUpdate.render diff contract', () => {
|
||||||
expect(diff.some(p => p.type === 'clearTerminal')).toBe(true)
|
expect(diff.some(p => p.type === 'clearTerminal')).toBe(true)
|
||||||
expect(stdoutOnly(diff)).toContain('timer2s')
|
expect(stdoutOnly(diff)).toContain('timer2s')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps DECSTBM fast-path when scroll region stays above bottom row', () => {
|
||||||
|
const w = 12
|
||||||
|
const h = 6
|
||||||
|
const prev = mkScreen(w, h)
|
||||||
|
const next = mkScreen(w, h)
|
||||||
|
|
||||||
|
paint(prev, 1, 'row one')
|
||||||
|
paint(next, 1, 'row one')
|
||||||
|
|
||||||
|
const prevFrame = mkFrame(prev, w, h)
|
||||||
|
const nextFrame: Frame = {
|
||||||
|
...mkFrame(next, w, h),
|
||||||
|
scrollHint: { top: 1, bottom: 4, delta: 1 }
|
||||||
|
}
|
||||||
|
const log = new LogUpdate({ isTTY: true, stylePool })
|
||||||
|
const diff = log.render(prevFrame, nextFrame, true, true)
|
||||||
|
|
||||||
|
expect(hasDecstbm(stdoutOnly(diff))).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips DECSTBM when scroll region touches the bottom row', () => {
|
||||||
|
const w = 12
|
||||||
|
const h = 6
|
||||||
|
const prev = mkScreen(w, h)
|
||||||
|
const next = mkScreen(w, h)
|
||||||
|
|
||||||
|
paint(prev, 1, 'row one')
|
||||||
|
paint(next, 1, 'row one')
|
||||||
|
|
||||||
|
const prevFrame = mkFrame(prev, w, h)
|
||||||
|
const nextFrame: Frame = {
|
||||||
|
...mkFrame(next, w, h),
|
||||||
|
scrollHint: { top: 1, bottom: 5, delta: 1 }
|
||||||
|
}
|
||||||
|
const log = new LogUpdate({ isTTY: true, stylePool })
|
||||||
|
const diff = log.render(prevFrame, nextFrame, true, true)
|
||||||
|
|
||||||
|
expect(hasDecstbm(stdoutOnly(diff))).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,10 @@ export class LogUpdate {
|
||||||
if (altScreen && next.scrollHint && decstbmSafe) {
|
if (altScreen && next.scrollHint && decstbmSafe) {
|
||||||
const { top, bottom, delta } = next.scrollHint
|
const { top, bottom, delta } = next.scrollHint
|
||||||
|
|
||||||
if (top >= 0 && bottom < prev.screen.height && bottom < next.screen.height) {
|
// Keep DECSTBM away from the terminal's last visible row. In alt-screen
|
||||||
|
// layouts we reserve that lane for status/cursor parking, and scrolling
|
||||||
|
// it can leave transient ghosting/bleed artifacts until a later repaint.
|
||||||
|
if (top >= 0 && bottom < prev.screen.height - 1 && bottom < next.screen.height - 1) {
|
||||||
shiftRows(prev.screen, top, bottom, delta)
|
shiftRows(prev.screen, top, bottom, delta)
|
||||||
scrollPatch = [
|
scrollPatch = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue