diff --git a/src/components/terminal/XTerminal.tsx b/src/components/terminal/XTerminal.tsx index 1a3e6060..6364506f 100644 --- a/src/components/terminal/XTerminal.tsx +++ b/src/components/terminal/XTerminal.tsx @@ -134,6 +134,7 @@ import { shouldBlockXTerminalData, } from "./xterminalKeyboardInput"; import { + clearTerminalAll, markTerminalUserInput, registerTerminalUserInputMarker, } from "@/lib/terminalControlInput"; @@ -1752,7 +1753,9 @@ export default function XTerminal({ if (appLockedRef.current) return; lineTimestampsRef.current = new Map(); gutterLineOffsetRef.current = 0; - terminal.reset(); + clearTerminalAll(terminal, { + shellRedraw: sessionTypeRef.current !== "Local" || !isWindows, + }); focusTerminal(); requestGutterRefresh(); }; diff --git a/src/lib/terminalControlInput.test.ts b/src/lib/terminalControlInput.test.ts index 3126be89..7aa96fec 100644 --- a/src/lib/terminalControlInput.test.ts +++ b/src/lib/terminalControlInput.test.ts @@ -1,6 +1,7 @@ import type { Terminal } from "@xterm/xterm"; import { describe, expect, it, vi } from "vitest"; import { + clearTerminalAll, registerTerminalUserInputMarker, sendTerminalClearInput, } from "./terminalControlInput"; @@ -24,4 +25,54 @@ describe("terminal control input origin", () => { expect(terminal.focus).toHaveBeenCalledOnce(); unregister(); }); + + it("clears scrollback without sending Ctrl+L or changing the CMD cursor row", async () => { + const canvas = vi + .spyOn(HTMLCanvasElement.prototype, "getContext") + .mockReturnValue({ + measureText: () => ({ width: 10 }), + } as unknown as CanvasRenderingContext2D); + const { Terminal: XTerminal } = await import("@xterm/xterm"); + const terminal = new XTerminal({ cols: 20, rows: 3 }); + await new Promise((resolve) => + terminal.write( + "first line\r\nold line\r\nsecond line\r\nprompt> abcdef\x1b[?1h", + resolve, + ), + ); + const input = vi.fn(); + const disposable = terminal.onData(input); + + expect(terminal.buffer.active.baseY).toBeGreaterThan(0); + clearTerminalAll(terminal); + await new Promise((resolve) => terminal.write("", resolve)); + + expect(terminal.buffer.active.length).toBe(3); + expect(terminal.buffer.active.baseY).toBe(0); + expect(terminal.buffer.active.cursorY).toBe(2); + expect(terminal.buffer.active.cursorX).toBe(14); + expect(terminal.buffer.active.getLine(0)?.translateToString(true)).toBe(""); + expect(terminal.buffer.active.getLine(1)?.translateToString(true)).toBe(""); + expect(terminal.buffer.active.getLine(2)?.translateToString(true)).toBe("prompt> abcdef"); + expect(input).not.toHaveBeenCalled(); + expect(terminal.modes.applicationCursorKeysMode).toBe(true); + + disposable.dispose(); + terminal.dispose(); + canvas.mockRestore(); + }); + + it("requests a redraw for shells that handle Ctrl+L", async () => { + const { Terminal: XTerminal } = await import("@xterm/xterm"); + const terminal = new XTerminal({ cols: 20, rows: 3 }); + await new Promise((resolve) => terminal.write("prompt> ", resolve)); + const input = vi.fn(); + const disposable = terminal.onData(input); + + clearTerminalAll(terminal, { shellRedraw: true }); + + expect(input).toHaveBeenCalledWith("\x0c"); + disposable.dispose(); + terminal.dispose(); + }); }); diff --git a/src/lib/terminalControlInput.ts b/src/lib/terminalControlInput.ts index f4ebc86f..9222a492 100644 --- a/src/lib/terminalControlInput.ts +++ b/src/lib/terminalControlInput.ts @@ -28,3 +28,18 @@ export function sendTerminalClearInput( terminal.focus(); } } + +export function clearTerminalAll( + terminal: Terminal, + options: { shellRedraw?: boolean } = {}, +): void { + const { cursorX, cursorY } = terminal.buffer.active; + terminal.clearSelection(); + terminal.clear(); + if (options.shellRedraw) { + sendTerminalClearInput(terminal); + } else if (cursorY > 0) { + // Keep ConPTY's viewport cursor coordinates while discarding old output. + terminal.write(`\x1b[1;1H\x1b[${cursorY}L\x1b[${cursorY + 1};${cursorX + 1}H`); + } +}