diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts index 3b2353912..55f91d547 100644 --- a/packages/tui/src/tui.ts +++ b/packages/tui/src/tui.ts @@ -413,9 +413,25 @@ function parseSizeValue(value: SizeValue | undefined, referenceSize: number): nu return undefined; } -/** Detect terminal multiplexers where scrollback clearing and height-change redraws are hostile. */ +/** + * Detect sessions where ED3 cannot safely rebuild scrollback. Direct HerdR + * panes support explicit clears; nested multiplexers remain unsafe because the + * inner tmux/screen/Zellij layer owns their history. + */ function isMultiplexerSession(): boolean { - return isInsideTerminalMultiplexer(); + if (!isInsideTerminalMultiplexer()) return false; + if (Bun.env.HERDR_ENV !== "1") return true; + const term = Bun.env.TERM?.toLowerCase() ?? ""; + return Boolean( + Bun.env.TMUX || + Bun.env.STY || + Bun.env.ZELLIJ || + Bun.env.CMUX_WORKSPACE_ID || + Bun.env.CMUX_SURFACE_ID || + Bun.env.CMUX_REMOTE_TRANSPORT || + term.startsWith("tmux") || + term.startsWith("screen"), + ); } /** @@ -439,12 +455,12 @@ function reportsSizeOnAltScreenToggle(): boolean { /** * Resize should repaint the visible window in place — no alternate-screen - * borrow, no ED3 scrollback rewrap — for multiplexer panes and for terminals - * that loop on alt-screen toggles. The tradeoff is identical to a multiplexer: - * scrollback above the window keeps its old wrap instead of being re-flowed. + * borrow, no ED3 scrollback rewrap — for multiplexer and direct HerdR panes, + * plus terminals that loop on alt-screen toggles. Direct HerdR remains a + * direct terminal for explicit transcript replacement and display reset. */ function resizeRepaintsInPlace(): boolean { - return isMultiplexerSession() || reportsSizeOnAltScreenToggle(); + return isMultiplexerSession() || Bun.env.HERDR_ENV === "1" || reportsSizeOnAltScreenToggle(); } /** diff --git a/packages/tui/test/issue-2088-repro.test.ts b/packages/tui/test/issue-2088-repro.test.ts index 1126c7ef1..3604ccfd3 100644 --- a/packages/tui/test/issue-2088-repro.test.ts +++ b/packages/tui/test/issue-2088-repro.test.ts @@ -2564,6 +2564,26 @@ describe("multiplexer detection gates ED3 on resize", () => { }); }); + it("preserves explicit scrollback clears in direct HerdR", async () => { + await withEnvPatch({ ...NO_MULTIPLEXER_ENV, TERM: "dumb", HERDR_ENV: "1" }, async () => { + const term = new VirtualTerminal(40, 10, 1000); + const tui = new TUI(term); + tui.addChild(new MutableLinesComponent(Array.from({ length: 20 }, (_value, index) => `line-${index}`))); + + try { + tui.start(); + await settle(term); + const writes = captureWrites(term); + tui.resetDisplay(); + await settle(term); + + expect(writes.join("")).toContain(ED3); + } finally { + tui.stop(); + } + }); + }); + it("keeps nested tmux inside HerdR on the ED3-unsafe path", async () => { await withEnvPatch({ ...TMUX_ENV, TERM: "tmux-256color", HERDR_ENV: "1" }, async () => { const term = new VirtualTerminal(40, 10, 1000);