mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 17:04:42 +00:00
fix(terminal): preserve mouse encoding when replaying sessions (#9004)
Restore SGR and pixel mouse encoding alongside tracking modes so Copilot CLI scrolling survives switching conversations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,13 +16,13 @@ export function createTerminalSerializer(
|
||||
// absolutely; an x==cols cursor must keep its pending wrap for the next byte.
|
||||
const position =
|
||||
buffer.cursorX < terminal.cols ? `\x1b[${buffer.cursorY + 1};${buffer.cursorX + 1}H` : "";
|
||||
return Buffer.from(data + position + serializeCursorPresentation(terminal), "utf8");
|
||||
return Buffer.from(data + position + serializeAdditionalModes(terminal), "utf8");
|
||||
};
|
||||
}
|
||||
|
||||
// Stable addon 0.14 serializes buffer/modes but omits DECSCUSR and cursor visibility.
|
||||
// xterm 6 keeps the program-selected presentation in coreService, not public options.
|
||||
function serializeCursorPresentation(terminal: TerminalType): string {
|
||||
// Addon 0.14 omits mouse encoding, DECSCUSR and cursor visibility. xterm 6
|
||||
// retains these in internal services rather than the public modes/options.
|
||||
function serializeAdditionalModes(terminal: TerminalType): string {
|
||||
const core = (
|
||||
terminal as unknown as {
|
||||
_core: {
|
||||
@@ -30,13 +30,17 @@ function serializeCursorPresentation(terminal: TerminalType): string {
|
||||
isCursorHidden: boolean;
|
||||
decPrivateModes: { cursorStyle?: "block" | "underline" | "bar"; cursorBlink?: boolean };
|
||||
};
|
||||
mouseStateService: { activeEncoding: string };
|
||||
};
|
||||
}
|
||||
)._core.coreService;
|
||||
const visibility = `\x1b[?25${core.isCursorHidden ? "l" : "h"}`;
|
||||
const cursorStyle = core.decPrivateModes.cursorStyle;
|
||||
)._core;
|
||||
let mouseEncoding = "";
|
||||
if (core.mouseStateService.activeEncoding === "SGR") mouseEncoding = "\x1b[?1006h";
|
||||
else if (core.mouseStateService.activeEncoding === "SGR_PIXELS") mouseEncoding = "\x1b[?1016h";
|
||||
const visibility = `${mouseEncoding}\x1b[?25${core.coreService.isCursorHidden ? "l" : "h"}`;
|
||||
const cursorStyle = core.coreService.decPrivateModes.cursorStyle;
|
||||
// Unset presentation belongs to each renderer's configured defaults.
|
||||
if (cursorStyle === undefined) return visibility;
|
||||
const style = { block: 2, underline: 4, bar: 6 }[cursorStyle];
|
||||
return `${visibility}\x1b[${style - (core.decPrivateModes.cursorBlink ? 1 : 0)} q`;
|
||||
return `${visibility}\x1b[${style - (core.coreService.decPrivateModes.cursorBlink ? 1 : 0)} q`;
|
||||
}
|
||||
|
||||
@@ -1702,6 +1702,52 @@ it.each([false, true])(
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "SGR", modes: "\x1b[?1006h", wheel: "\x1b[<64;10;5M" },
|
||||
{ name: "SGR pixels", modes: "\x1b[?1016h", wheel: "\x1b[<64;80;90M" },
|
||||
{ name: "disabled SGR", modes: "\x1b[?1006h\x1b[?1006l", wheel: "\x1b[M`*%" },
|
||||
])("preserves $name wheel reports across repeated terminal replays", async (input) => {
|
||||
const source = new xterm.Terminal({ rows: 10, cols: 80, allowProposedApi: true });
|
||||
const receiver = new xterm.Terminal({ rows: 10, cols: 80, allowProposedApi: true });
|
||||
const serialize = createTerminalSerializer(source);
|
||||
const wheel = {
|
||||
button: 4,
|
||||
action: 0,
|
||||
col: 10,
|
||||
row: 5,
|
||||
x: 80,
|
||||
y: 90,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
};
|
||||
// Mouse encoding is not exposed by xterm's public modes API.
|
||||
const mouse = (
|
||||
receiver as unknown as {
|
||||
_core: {
|
||||
mouseStateService: {
|
||||
encodeMouseEvent(event: typeof wheel): string;
|
||||
};
|
||||
};
|
||||
}
|
||||
)._core.mouseStateService;
|
||||
try {
|
||||
await new Promise<void>((resolve) =>
|
||||
source.write("\x1b[?1049h\x1b[?1002h" + input.modes + "TRANSCRIPT", resolve),
|
||||
);
|
||||
for (let replay = 0; replay < 2; replay++) {
|
||||
receiver.reset();
|
||||
await new Promise<void>((resolve) => receiver.write(serialize(), resolve));
|
||||
expect(receiver.buffer.active.type).toBe("alternate");
|
||||
expect(receiver.modes.mouseTrackingMode).toBe("drag");
|
||||
expect(mouse.encodeMouseEvent(wheel)).toBe(input.wheel);
|
||||
}
|
||||
} finally {
|
||||
receiver.dispose();
|
||||
source.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "unset", presentation: "", style: undefined, visible: true },
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user