Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-terminal-input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Prevent terminal status responses from interrupting active turns.
14 changes: 11 additions & 3 deletions packages/pi-tui/src/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
} from "./utils.ts";

const KITTY_SEQUENCE_PREFIX = "\x1b_G";
const TERMINAL_STATUS_RESPONSE_PATTERN =
/^(?:\x1b\[\?[\d;]*c|\x1b(?:P.*\x1b\\|_.*\x1b\\))$/s;
const OSC_STATUS_RESPONSE_PATTERN = /^\x1b\].*(?:\x07|\x1b\\)$/s;

/** Shared empty id list for non-image lines in the per-line image-id cache. */
const EMPTY_IMAGE_IDS: readonly number[] = [];
Expand Down Expand Up @@ -788,6 +791,11 @@ export class TUI extends Container {
if (this.consumeTerminalColorSchemeReport(data)) {
return;
}
// Terminal query replies are control traffic, not keyboard input. Consume
// known replies before global listeners can interpret them as user intent.
if (this.consumeCellSizeResponse(data) || TERMINAL_STATUS_RESPONSE_PATTERN.test(data)) {
return;
}

if (this.inputListeners.size > 0) {
let current = data;
Expand All @@ -805,9 +813,9 @@ export class TUI extends Container {
}
data = current;
}

// Consume terminal cell size responses without blocking unrelated input.
if (this.consumeCellSizeResponse(data)) {
// OSC reports may be consumed by application listeners (for example,
// live theme tracking), but must never fall through to focused input.
if (OSC_STATUS_RESPONSE_PATTERN.test(data)) {
return;
}

Expand Down
88 changes: 88 additions & 0 deletions packages/pi-tui/test/terminal-colors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,94 @@ describe("parseTerminalColorSchemeReport", () => {
});

describe("TUI.queryTerminalBackgroundColor", () => {
it("filters non-OSC terminal status responses before application dispatch", () => {
const terminal = new TestTerminal();
const tui = new TUI(terminal);
const component = new InputRecorder();
const listenerInputs: string[] = [];
tui.addChild(component);
tui.setFocus(component);
tui.addInputListener((data) => {
listenerInputs.push(data);
return undefined;
});
tui.start();
try {
terminal.sendInput("\x1b[6;18;8t");
terminal.sendInput("\x1b[?1;2;4c");
terminal.sendInput("\x1bP>|XTerm(390)\x1b\\");
terminal.sendInput("\x1b_Gi=1;OK\x1b\\");

assert.deepStrictEqual(listenerInputs, []);
assert.deepStrictEqual(component.inputs, []);
} finally {
tui.stop();
}
});

it("lets protocol listeners consume OSC status before focused input", () => {
const terminal = new TestTerminal();
const tui = new TUI(terminal);
const component = new InputRecorder();
const oscStatusResponse = "\x1b]11;rgb:1515/1919/1e1e\x1b\\";
let observedStatus: string | undefined;
tui.addChild(component);
tui.setFocus(component);
tui.addInputListener((data) => {
if (data !== oscStatusResponse) return undefined;
observedStatus = data;
return { consume: true };
});
tui.start();
try {
terminal.sendInput(oscStatusResponse);

assert.equal(observedStatus, oscStatusResponse);
assert.deepStrictEqual(component.inputs, []);
} finally {
tui.stop();
}
});

it("filters unhandled OSC status before focused input", () => {
const terminal = new TestTerminal();
const tui = new TUI(terminal);
const component = new InputRecorder();
tui.addChild(component);
tui.setFocus(component);
tui.start();
try {
terminal.sendInput("\x1b]11;rgb:1515/1919/1e1e\x1b\\");

assert.deepStrictEqual(component.inputs, []);
} finally {
tui.stop();
}
});

it("preserves keyboard and mouse input after filtering terminal status responses", () => {
const terminal = new TestTerminal();
const tui = new TUI(terminal);
const component = new InputRecorder();
const listenerInputs: string[] = [];
tui.addChild(component);
tui.setFocus(component);
tui.addInputListener((data) => {
listenerInputs.push(data);
return undefined;
});
tui.start();
try {
const inputSequences = ["a", "\x1b[<0;10;5M", "\x1b[97u"];
for (const input of inputSequences) terminal.sendInput(input);

assert.deepStrictEqual(listenerInputs, inputSequences);
assert.deepStrictEqual(component.inputs, inputSequences);
} finally {
tui.stop();
}
});

it("writes OSC 11 query and resolves with the parsed RGB reply", async () => {
const terminal = new TestTerminal();
const tui = new TUI(terminal);
Expand Down