Fix terminal color query replies

Answer OSC color queries from the daemon and suppress browser-side replies so response bytes do not race back into the shell prompt as user input.
This commit is contained in:
Mohamed Boudra
2026-05-26 09:32:15 +07:00
parent 48d4e3e408
commit 6e56ee9e32
5 changed files with 136 additions and 0 deletions
@@ -176,6 +176,26 @@ process.stdin.on("data", (chunk) => {
process.stdout.write(query);
`;
const OSC11_HELPER_SCRIPT = `process.stdin.setRawMode(true);
process.stdin.resume();
let buf = "";
const timer = setTimeout(() => {
process.stdout.write("OSC11_TIMEOUT\\n");
process.exit(2);
}, 2500);
process.stdin.on("data", (chunk) => {
buf += chunk.toString("binary");
const match = buf.match(/\\x1b\\]11;rgb:[0-9a-f/]+\\x1b\\\\/);
if (!match) {
return;
}
clearTimeout(timer);
process.stdout.write("OSC11_OK:" + match[0].replace(/\\x1b/g, "ESC") + "\\n");
process.exit(0);
});
process.stdout.write("\\x1b]11;?\\x07");
`;
function writeDaHelper(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix));
temporaryDirs.push(dir);
@@ -192,6 +212,14 @@ function writeDsrHelper(prefix: string): string {
return path;
}
function writeOsc11Helper(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix));
temporaryDirs.push(dir);
const path = join(dir, "helper.cjs");
writeFileSync(path, OSC11_HELPER_SCRIPT);
return path;
}
function isDaOkLine(line: string): boolean {
return line.startsWith("DA_OK:");
}
@@ -200,6 +228,10 @@ function isDsrOkLine(line: string): boolean {
return line.startsWith("DSR_OK:");
}
function isOsc11OkLine(line: string): boolean {
return line.startsWith("OSC11_OK:");
}
function hasDaOkLine(state: ReturnType<TerminalSession["getState"]>): boolean {
return getLines(state).some(isDaOkLine);
}
@@ -208,6 +240,10 @@ function hasDsrOkLine(state: ReturnType<TerminalSession["getState"]>): boolean {
return getLines(state).some(isDsrOkLine);
}
function hasOsc11OkLine(state: ReturnType<TerminalSession["getState"]>): boolean {
return getLines(state).some(isOsc11OkLine);
}
function lastNonEmptyLineIsPrompt(state: ReturnType<TerminalSession["getState"]>): boolean {
const last =
getLines(state)
@@ -982,6 +1018,25 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
const ack = getLines(session.getState()).find(isDsrOkLine) ?? "";
expect(ack).toBe("DSR_OK:status");
});
it("delivers OSC 11 background-color replies to a foreground app on stdin", async () => {
const helperPath = writeOsc11Helper("terminal-osc11-helper-");
const session = trackSession(
await createTerminal({
cwd: "/tmp",
shell: "/bin/sh",
env: { PS1: "$ " },
}),
);
await waitForLines(session, ["$"]);
session.send({ type: "input", data: `${process.execPath} ${helperPath}\r` });
await waitForState(session, hasOsc11OkLine);
const ack = getLines(session.getState()).find(isOsc11OkLine) ?? "";
expect(ack).toBe("OSC11_OK:ESC]11;rgb:0b0b/0b0b/0b0bESC\\");
});
});
describe("stream snapshots", () => {
+14
View File
@@ -17,6 +17,11 @@ let nodePtySpawnHelperChecked = false;
const TERMINAL_TITLE_DEBOUNCE_MS = 150;
const TERMINAL_EXIT_OUTPUT_LINE_LIMIT = 12;
const TERMINAL_EXIT_OUTPUT_CHAR_LIMIT = 16000;
const TERMINAL_OSC_COLOR_QUERY_RESPONSES = new Map<number, string>([
[10, "rgb:e6e6/e6e6/e6e6"],
[11, "rgb:0b0b/0b0b/0b0b"],
[12, "rgb:e6e6/e6e6/e6e6"],
]);
export interface TerminalExitInfo {
exitCode: number | null;
@@ -681,6 +686,15 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
ptyProcess.write(`\x1b[?${buffer.cursorY + 1};${buffer.cursorX + 1}R`);
return true;
});
for (const [code, response] of TERMINAL_OSC_COLOR_QUERY_RESPONSES) {
terminal.parser.registerOscHandler(code, (data) => {
if (data.trim() !== "?") {
return false;
}
ptyProcess.write(`\x1b]${code};${response}\x1b\\`);
return true;
});
}
if (titleMode === "auto") {
titleChangeSubscription = terminal.onTitleChange((nextTitle) => {