Compare commits

...
Author SHA1 Message Date
陈大猫andsi1enteee 98c4efb6ff test(terminal): exercise native synchronized-output erase handling
Read xterm's synchronized-output mode in the erase handler itself, remove
unused custom tracking, and cover combined modes and split writes.

Co-authored-by: si1enteee <si1enteee@users.noreply.github.com>
2026-10-08 14:36:48 +08:00
netcatty-bot 940bddf5b2 fix: address Codex review on PR #3596 2026-10-08 05:52:22 +00:00
netcatty-bot 7ad3263fa4 fix: address Codex review on PR #3596 2026-10-08 05:24:51 +00:00
netcatty-bot c0d66cc8a6 fix: address Codex review on PR #3596 2026-10-08 04:56:00 +00:00
netcatty-bot 28b9db7e81 fix: address Codex review on PR #3596 2026-10-08 04:26:07 +00:00
netcatty-bot ff4a48916f fix(#3595): automated AI fix 2026-10-08 04:03:09 +00:00
3 changed files with 96 additions and 27 deletions

No files matched your search

@@ -66,6 +66,7 @@ const createEraseHandlerHarness = (
};
const observedScrollRegions: Array<[number, number]> = [];
const term = {
modes: { synchronizedOutputMode: options.inDec2026SyncBlock ?? false },
rows: 5,
options: {
scrollOnEraseInDisplay: false,
@@ -118,7 +119,6 @@ const createEraseHandlerHarness = (
};
const disposable = installEraseInDisplayHandlers(term as never, {
getClearWipesScrollback: () => options.clearWipesScrollback ?? false,
isInDec2026SyncBlock: () => options.inDec2026SyncBlock ?? false,
scheduleMicrotask: (callback) => {
microtasks.push(callback);
},
@@ -321,7 +321,6 @@ test("erase-below wipe preserves later output from the same write batch", async
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
const disposable = installEraseInDisplayHandlers(term as never, {
getClearWipesScrollback: () => true,
isInDec2026SyncBlock: () => false,
});
await writeTerminal(term, "old1\r\nold2\r\nold3\r\nold4\r\nold5\r\nold6\r\nold7\r\nold8");
@@ -524,3 +523,96 @@ test("appendEraseScrollback still wipes when delayed clear is outside a sync blo
"\x1b[H\x1b[2J\x1b[3Jframe",
);
});
const getScrollbackLines = (term: InstanceType<typeof Terminal>): string[] =>
Array.from({ length: term.buffer.active.baseY }, (_, row) =>
term.buffer.active.getLine(row)?.translateToString(true) ?? ""
);
test("native synchronized-output mode ignores unrelated private modes", async () => {
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
await writeTerminal(term, "\x1b[?2004h");
assert.equal(term.modes.synchronizedOutputMode, false, "bracketed paste 2004 must not open a sync block");
await writeTerminal(term, "\x1b[?25h\x1b[?1049h");
assert.equal(term.modes.synchronizedOutputMode, false, "cursor/alt-screen modes must not open a sync block");
await writeTerminal(term, "\x1b[?25:2026h");
assert.equal(term.modes.synchronizedOutputMode, false, "subparameters do not enable sync mode");
await writeTerminal(term, "\x1b[?25;2026h");
assert.equal(term.modes.synchronizedOutputMode, true, "combined primary modes enable sync mode");
await writeTerminal(term, "\x1b[?2004l");
assert.equal(term.modes.synchronizedOutputMode, true, "closing 2004 must not close the 2026 block");
await writeTerminal(term, "\x1b[?2026l");
assert.equal(term.modes.synchronizedOutputMode, false);
term.dispose();
});
test("Ctrl+L clear survives a bash prompt (2004 open, 2026 closed)", async () => {
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
const erase = installEraseInDisplayHandlers(term as never, {
getClearWipesScrollback: () => false,
});
// Exactly two rows already in scrollback, then a full screen of visible rows.
await writeTerminal(term, "old1\r\nold2\r\nvis1\r\nvis2\r\nvis3\r\nvis4\r\nvis5");
assert.equal(term.buffer.active.baseY, 2);
// bash 5.1 at a prompt holds bracketed paste open; Ctrl+L only sends CSI 2 J.
await writeTerminal(term, "\x1b[?2004h");
await writeTerminal(term, "\x1b[H\x1b[2Jnew1");
const scrollback = getScrollbackLines(term);
assert.equal(scrollback.some((line) => line.startsWith("vis")), true, "visible rows preserved in scrollback");
assert.equal(scrollback.some((line) => line.startsWith("old")), true, "earlier history not trimmed");
erase.dispose();
term.dispose();
});
test("clears inside a DEC 2026 sync block still erase in place", async () => {
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
const erase = installEraseInDisplayHandlers(term as never, {
getClearWipesScrollback: () => false,
});
await writeTerminal(term, "old1\r\nold2\r\nvis1\r\nvis2\r\nvis3\r\nvis4\r\nvis5");
assert.equal(term.buffer.active.baseY, 2);
await writeTerminal(term, "\x1b[?2026h\x1b[H\x1b[2Jnew1\x1b[?2026l");
const scrollback = getScrollbackLines(term);
assert.equal(scrollback.some((line) => line.startsWith("vis")), false, "sync-block erase stays in place");
assert.equal(scrollback.some((line) => line.startsWith("old")), true, "only pre-existing scrollback remains");
erase.dispose();
term.dispose();
});
for (const [label, sequence, preservesViewport] of [
["combined primary modes", "\x1b[?25;2026h", false],
["subparameter only", "\x1b[?25:2026h", true],
["explicit reset", "\x1b[?2026h\x1b[?2026l", true],
] as const) {
test(`full erase uses native synchronized-output state for ${label}`, async () => {
const term = new Terminal({ cols: 20, rows: 5, scrollback: 100 });
const erase = installEraseInDisplayHandlers(term, {
getClearWipesScrollback: () => false,
});
try {
await writeTerminal(term, "old1\r\nold2\r\nvis1\r\nvis2\r\nvis3\r\nvis4\r\nvis5");
// The mode and erase arrive in separate PTY chunks, as with TUI redraws.
await writeTerminal(term, sequence);
await writeTerminal(term, "\x1b[H\x1b[2Jnew1");
assert.equal(getScrollbackLines(term).some((line) => line.startsWith("vis")), preservesViewport);
assert.equal(getScrollbackLines(term).some((line) => line.startsWith("old")), true);
} finally {
erase.dispose();
term.dispose();
}
});
}
+2 -3
View File
@@ -47,7 +47,6 @@ type AppendEraseScrollbackOptions = {
type EraseInDisplayHandlerOptions = {
getClearWipesScrollback: () => boolean;
isInDec2026SyncBlock: () => boolean;
scheduleMicrotask?: (callback: () => void) => void;
};
@@ -264,7 +263,6 @@ export const installEraseInDisplayHandlers = (
term: EraseInDisplayTerminal,
{
getClearWipesScrollback,
isInDec2026SyncBlock,
scheduleMicrotask = queueMicrotask,
}: EraseInDisplayHandlerOptions,
): IDisposable => {
@@ -279,7 +277,8 @@ export const installEraseInDisplayHandlers = (
const eraseDisposable = term.parser.registerCsiHandler({ final: "J" }, (params) => {
const wipeAllowed = getClearWipesScrollback();
const inDec2026SyncBlock = isInDec2026SyncBlock();
// Use xterm's parsed mode, including its native safety timeout.
const inDec2026SyncBlock = term.modes.synchronizedOutputMode;
// Scope xterm's native preservation to shell clears, not TUI redraws.
if (isEraseViewportSequence(params)) {
const useNativeScrollPreservation = shouldScrollOnEraseInDisplay(
@@ -3170,28 +3170,8 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
// OSC 7 format: \x1b]7;file://hostname/path\x07 or \x1b]7;file://hostname/path\x1b\\
let currentCwd: string | undefined = undefined;
// Track DEC 2026 synchronized-output blocks so CSI 2 J can erase in place for
// Codex/Claude Code TUIs instead of pushing visible rows into scrollback.
let inDec2026SyncBlock = false;
const dec2026SyncStartDisposable = term.parser.registerCsiHandler(
{ prefix: "?", final: "h", params: [2026] },
() => {
inDec2026SyncBlock = true;
return false;
},
);
const dec2026SyncEndDisposable = term.parser.registerCsiHandler(
{ prefix: "?", final: "l", params: [2026] },
() => {
inDec2026SyncBlock = false;
return false;
},
);
const eraseScrollbackDisposable = installEraseInDisplayHandlers(term, {
getClearWipesScrollback: () => ctx.terminalSettingsRef.current?.clearWipesScrollback ?? true,
isInDec2026SyncBlock: () => inDec2026SyncBlock,
});
const markCursorPositionReportRequest = (params: readonly (number | number[])[]): boolean => {
@@ -3500,8 +3480,6 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
pluginLinkProviderHost?.dispose();
pluginProviderHost?.dispose();
eraseScrollbackDisposable.dispose();
dec2026SyncStartDisposable.dispose();
dec2026SyncEndDisposable.dispose();
for (const disposable of cursorPositionReportRequestDisposables) {
disposable.dispose();
}