mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): prevent split Alt+Enter from interrupting (#7899)
* fix(tui): prevent split Alt+Enter from interrupting Increase the escape-sequence timeout over SSH and allow high-latency terminals to override it with PI_TUI_ESC_TIMEOUT. Fixes #7876 * docs(coding-agent): shorten escape timeout description --------- Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
parent
230029078d
commit
06ed871679
4 changed files with 81 additions and 3 deletions
|
|
@ -101,6 +101,25 @@ export interface Terminal {
|
|||
setProgress(active: boolean): void;
|
||||
}
|
||||
|
||||
const DEFAULT_ESCAPE_TIMEOUT_MS = 10;
|
||||
const DEFAULT_SSH_ESCAPE_TIMEOUT_MS = 100;
|
||||
|
||||
/**
|
||||
* Resolve how long to wait for the rest of an escape sequence before
|
||||
* dispatching a lone ESC as the Escape key. Legacy Alt+key input is ESC plus
|
||||
* another byte, so high-latency transports need a longer reassembly window.
|
||||
*/
|
||||
export function resolveEscapeTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
||||
const configured = Number(env.PI_TUI_ESC_TIMEOUT);
|
||||
if (Number.isFinite(configured) && configured > 0) {
|
||||
return configured;
|
||||
}
|
||||
if (env.SSH_CONNECTION || env.SSH_TTY) {
|
||||
return DEFAULT_SSH_ESCAPE_TIMEOUT_MS;
|
||||
}
|
||||
return DEFAULT_ESCAPE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Real terminal using process.stdin/stdout
|
||||
*/
|
||||
|
|
@ -183,7 +202,7 @@ export class ProcessTerminal implements Terminal {
|
|||
* to handle the case where the response arrives split across multiple events.
|
||||
*/
|
||||
private setupStdinBuffer(): void {
|
||||
this.stdinBuffer = new StdinBuffer();
|
||||
this.stdinBuffer = new StdinBuffer({ timeout: resolveEscapeTimeoutMs() });
|
||||
|
||||
// Forward individual sequences to the input handler
|
||||
this.stdinBuffer.on("data", (sequence) => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import assert from "node:assert";
|
||||
import { beforeEach, describe, it } from "node:test";
|
||||
import { matchesKey } from "../src/keys.ts";
|
||||
import { StdinBuffer } from "../src/stdin-buffer.ts";
|
||||
|
||||
describe("StdinBuffer", () => {
|
||||
|
|
@ -134,6 +135,35 @@ describe("StdinBuffer", () => {
|
|||
assert.deepStrictEqual(emittedSequences, ["\x1b[<35"]);
|
||||
});
|
||||
|
||||
|
||||
it("should flush a lone ESC as Escape when CR arrives after the timeout", async () => {
|
||||
// Legacy-mode Alt+Enter is ESC + CR; when the terminal/transport splits
|
||||
// the bytes further apart than the timeout, ESC is flushed alone and the
|
||||
// host sees Escape (interrupt) instead of Alt+Enter. This locks in the
|
||||
// behavior so the configurable timeout in ProcessTerminal stays honest.
|
||||
processInput("\x1b");
|
||||
await wait(20); // buffer timeout is 10ms in beforeEach
|
||||
processInput("\r");
|
||||
|
||||
assert.deepStrictEqual(emittedSequences, ["\x1b", "\r"]);
|
||||
assert.equal(matchesKey(emittedSequences[0] ?? "", "escape"), true);
|
||||
});
|
||||
|
||||
it("should merge ESC + CR split across chunks within a larger timeout", async () => {
|
||||
buffer = new StdinBuffer({ timeout: 100 });
|
||||
emittedSequences = [];
|
||||
buffer.on("data", (sequence) => {
|
||||
emittedSequences.push(sequence);
|
||||
});
|
||||
|
||||
processInput("\x1b");
|
||||
await wait(20); // > 10ms old default, < 100ms configured timeout
|
||||
processInput("\r");
|
||||
|
||||
assert.deepStrictEqual(emittedSequences, ["\x1b\r"]);
|
||||
assert.equal(matchesKey(emittedSequences[0] ?? "", "alt+enter"), true);
|
||||
});
|
||||
|
||||
it("keeps fragmented mouse sequences buffered across delayed chunks by default", async () => {
|
||||
const delayedBuffer = new StdinBuffer();
|
||||
const delayedSequences: string[] = [];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,35 @@
|
|||
import assert from "node:assert";
|
||||
import { describe, it, mock } from "node:test";
|
||||
import { setKittyProtocolActive } from "../src/keys.ts";
|
||||
import { normalizeAppleTerminalInput, normalizeNativeShiftEnterInput, ProcessTerminal } from "../src/terminal.ts";
|
||||
import {
|
||||
normalizeAppleTerminalInput,
|
||||
normalizeNativeShiftEnterInput,
|
||||
ProcessTerminal,
|
||||
resolveEscapeTimeoutMs,
|
||||
} from "../src/terminal.ts";
|
||||
|
||||
describe("resolveEscapeTimeoutMs", () => {
|
||||
it("uses PI_TUI_ESC_TIMEOUT when configured", () => {
|
||||
assert.equal(resolveEscapeTimeoutMs({ PI_TUI_ESC_TIMEOUT: "80" }), 80);
|
||||
assert.equal(resolveEscapeTimeoutMs({ PI_TUI_ESC_TIMEOUT: "80", SSH_TTY: "/dev/pts/1" }), 80);
|
||||
});
|
||||
|
||||
it("ignores invalid PI_TUI_ESC_TIMEOUT values", () => {
|
||||
assert.equal(resolveEscapeTimeoutMs({ PI_TUI_ESC_TIMEOUT: "abc" }), 10);
|
||||
assert.equal(resolveEscapeTimeoutMs({ PI_TUI_ESC_TIMEOUT: "0" }), 10);
|
||||
assert.equal(resolveEscapeTimeoutMs({ PI_TUI_ESC_TIMEOUT: "-5" }), 10);
|
||||
assert.equal(resolveEscapeTimeoutMs({ PI_TUI_ESC_TIMEOUT: "" }), 10);
|
||||
});
|
||||
|
||||
it("defaults to 100ms over SSH", () => {
|
||||
assert.equal(resolveEscapeTimeoutMs({ SSH_CONNECTION: "10.0.0.1 22" }), 100);
|
||||
assert.equal(resolveEscapeTimeoutMs({ SSH_TTY: "/dev/pts/1" }), 100);
|
||||
});
|
||||
|
||||
it("defaults to 10ms otherwise", () => {
|
||||
assert.equal(resolveEscapeTimeoutMs({}), 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeNativeShiftEnterInput", () => {
|
||||
it("rewrites Return to CSI-u Shift+Enter when native Shift detection is enabled and Shift is pressed", () => {
|
||||
|
|
@ -195,7 +223,7 @@ describe("ProcessTerminal Kitty keyboard protocol negotiation", () => {
|
|||
const harness = setupNegotiation();
|
||||
try {
|
||||
harness.send("\x1b[");
|
||||
mock.timers.tick(10);
|
||||
mock.timers.tick(resolveEscapeTimeoutMs());
|
||||
|
||||
assert.equal(harness.getInput(), undefined);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue