diff --git a/packages/runtime-tests/integration/pty/issue-450-inline.test.tsx b/packages/runtime-tests/integration/pty/issue-450-inline.test.tsx index 57c966d..6405e95 100644 --- a/packages/runtime-tests/integration/pty/issue-450-inline.test.tsx +++ b/packages/runtime-tests/integration/pty/issue-450-inline.test.tsx @@ -56,7 +56,7 @@ it("#450: non-TTY overflow transitions should never clear terminal", async () => expect(clearCount).toBe(0); }); -it("#450: viewport shrink into overflow should clear once", async () => { +it("#450: viewport shrink into overflow clears exactly once on resize", async () => { const stdout = makeFakeWritable({ rows: 10 }); const stderr = makeFakeWritable(); const { stream: stdin } = makeFakeStdin(); @@ -69,15 +69,20 @@ it("#450: viewport shrink into overflow should clear once", async () => { app.mount({ stdout, stdin, stderr, exitOnCtrlC: false }); await nextTick(); await nextTick(); + const clearsBeforeResize = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length; + expect(clearsBeforeResize).toBe(0); // 8 lines fit in 10 rows — no clear yet + // Shrink so the content overflows the viewport. The resize renders + // synchronously (matching Ink), so the overflow clear is emitted right here, + // not deferred through the commit throttle. stdout.rows = 4; stdout.emit("resize"); await nextTick(); - await nextTick(); + + const clearsAfterResize = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length; + expect(clearsAfterResize - clearsBeforeResize).toBe(1); app.unmount(); - const clearCount = writes.filter((w) => w.includes(ansiEscapes.clearTerminal)).length; - expect(clearCount).toBe(1); }); it("#450: non-TTY grow-to-overflow rerender should not clear terminal", async () => { diff --git a/packages/runtime/src/render.ts b/packages/runtime/src/render.ts index eaea8e3..c666db2 100644 --- a/packages/runtime/src/render.ts +++ b/packages/runtime/src/render.ts @@ -658,8 +658,14 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp } // Only listen for resize in interactive mode (matching Ink). + // Render synchronously on resize rather than through the commit throttle: + // a resize is a discrete event that changes the viewport, and Ink's + // resized() handler calls onRender() directly. Deferring it through the + // ~32ms throttle can leave stale/overlapping content on screen for a frame + // and makes the clearTerminal-on-overflow behavior depend on wall-clock + // timing rather than the resize itself. if (interactive) { - const onResize = () => scheduler.schedule(); + const onResize = () => commit(); stdout.on("resize", onResize); mountedResizeHandler = onResize; }