renderer: fix split-footer resize cleanup (#1488)

A width shrink could clear below a settling footer, leaving stale rows
visible. Clear from the visible footer origin while preserving pinned
cleanup.

Supersede #1487
Fix #1486
This commit is contained in:
Simon Klee authored and GitHub committed 2026-09-09 12:45:31 +02:00
1 parent 97a20ab249
commit ac753b48d3
2 files changed
+97 -3

No files matched your search

+8 -3
View File
@@ -3996,12 +3996,17 @@ export class CliRenderer extends EventEmitter implements RenderContext {
const splitFooterActive = this._screenMode === "split-footer"
if (splitFooterActive) {
// Width shrink historically needs a broader scrub band, but if resize interrupts
// a deferred footer transition we also need to clear from that visible source surface.
// Width shrink historically needs a broader bottom-anchored scrub band so wrapped
// footer cells above a pinned surface are erased. Never start that clear below the
// visible footer origin, or a settling footer leaves stale rows. Interrupted
// transitions may still have an earlier source that must also be included.
let clearStart: number | null = null
if (width < prevWidth && visiblePreviousSplitHeight > 0) {
clearStart = Math.max(previousTerminalHeight - visiblePreviousSplitHeight * 2, 1)
clearStart = Math.min(
this.renderOffset + 1,
Math.max(previousTerminalHeight - visiblePreviousSplitHeight * 2, 1),
)
}
if (pendingSplitFooterTransition !== null) {
@@ -1774,6 +1774,95 @@ test("CliRenderer split-footer resize cleanup uses the visible footer surface wh
writeOutSpy.mockRestore()
})
test("CliRenderer split-footer width shrink clears from the visible settling footer origin", async () => {
const result = await createTestRenderer({
width: 80,
height: 24,
screenMode: "split-footer",
footerHeight: 6,
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
})
renderer = result.renderer
;(renderer as any)._terminalIsSetup = true
expect((renderer as any).renderOffset).toBe(1)
const writeOutSpy = spyOn(renderer as any, "writeOut")
result.resize(44, 22)
expect(writeOutSpy).toHaveBeenCalledTimes(1)
expect(writeOutSpy.mock.calls[0]?.[0]).toBe(ANSI.moveCursorAndClear(2, 1))
writeOutSpy.mockRestore()
})
test("CliRenderer split-footer width shrink clears from a mid-settling footer origin above the bottom-anchored band", async () => {
const result = await createTestRenderer({
width: 80,
height: 24,
screenMode: "split-footer",
footerHeight: 6,
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
})
renderer = result.renderer
;(renderer as any)._terminalIsSetup = true
;(renderer as any).stdout.write("a\n")
await result.renderOnce()
;(renderer as any).stdout.write("b\n")
await result.renderOnce()
;(renderer as any).stdout.write("c\n")
await result.renderOnce()
;(renderer as any).stdout.write("d\n")
await result.renderOnce()
expect((renderer as any).renderOffset).toBe(5)
const writeOutSpy = spyOn(renderer as any, "writeOut")
result.resize(44, 24)
expect(writeOutSpy).toHaveBeenCalledTimes(1)
expect(writeOutSpy.mock.calls[0]?.[0]).toBe(ANSI.moveCursorAndClear(6, 1))
writeOutSpy.mockRestore()
})
test("CliRenderer split-footer width shrink retains the broader scrub band when pinned", async () => {
const result = await createTestRenderer({
width: 80,
height: 24,
screenMode: "split-footer",
footerHeight: 6,
externalOutputMode: "capture-stdout",
consoleMode: "disabled",
})
renderer = result.renderer
;(renderer as any)._terminalIsSetup = true
for (let i = 0; i < 20; i++) {
;(renderer as any).stdout.write(`line-${i}\n`)
await result.renderOnce()
}
expect((renderer as any).renderOffset).toBe(18)
const writeOutSpy = spyOn(renderer as any, "writeOut")
result.resize(44, 24)
expect(writeOutSpy).toHaveBeenCalledTimes(1)
expect(writeOutSpy.mock.calls[0]?.[0]).toBe(ANSI.moveCursorAndClear(12, 1))
writeOutSpy.mockRestore()
})
test("CliRenderer split-footer resize cleanup uses the visible source top line across width and height resize while a deferred footer transition is pending", async () => {
const result = await createTestRenderer({
width: 40,