mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 08:00:59 +00:00
fix(tui): remove Kitty protocol query timeout
The 100ms timeout was causing Kitty protocol detection to fail when the terminal response was delayed (e.g., due to event loop blocking during startup). This resulted in shift+enter not working in some scenarios. Changes: - Remove timeout-based Kitty detection, process input immediately - Detect Kitty response in stdinBuffer output (handles split data) - Add modifyOtherKeys fallback for terminals without Kitty support (matches xterm format \x1b[27;modifier;keycode~)
This commit is contained in:
parent
f1e225d9e7
commit
c07126c0fd
2 changed files with 48 additions and 73 deletions
|
|
@ -437,6 +437,21 @@ function matchesKittySequence(data: string, expectedCodepoint: number, expectedM
|
|||
return parsed.codepoint === expectedCodepoint && actualMod === expectedMod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match xterm modifyOtherKeys format: CSI 27 ; modifiers ; keycode ~
|
||||
* This is used by terminals when Kitty protocol is not enabled.
|
||||
* Modifier values are 1-indexed: 2=shift, 3=alt, 5=ctrl, etc.
|
||||
*/
|
||||
function matchesModifyOtherKeys(data: string, expectedKeycode: number, expectedModifier: number): boolean {
|
||||
const match = data.match(/^\x1b\[27;(\d+);(\d+)~$/);
|
||||
if (!match) return false;
|
||||
const modValue = parseInt(match[1]!, 10);
|
||||
const keycode = parseInt(match[2]!, 10);
|
||||
// Convert from 1-indexed xterm format to our 0-indexed format
|
||||
const actualMod = modValue - 1;
|
||||
return keycode === expectedKeycode && actualMod === expectedModifier;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Generic Key Matching
|
||||
// =============================================================================
|
||||
|
|
@ -515,6 +530,10 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
|
|||
) {
|
||||
return true;
|
||||
}
|
||||
// xterm modifyOtherKeys format (fallback when Kitty protocol not enabled)
|
||||
if (matchesModifyOtherKeys(data, CODEPOINTS.enter, MODIFIERS.shift)) {
|
||||
return true;
|
||||
}
|
||||
// When Kitty protocol is active, legacy sequences are custom terminal mappings
|
||||
// \x1b\r = Kitty's "map shift+enter send_text all \e\r"
|
||||
// \n = Ghostty's "keybind = shift+enter=text:\n"
|
||||
|
|
@ -531,6 +550,10 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
|
|||
) {
|
||||
return true;
|
||||
}
|
||||
// xterm modifyOtherKeys format (fallback when Kitty protocol not enabled)
|
||||
if (matchesModifyOtherKeys(data, CODEPOINTS.enter, MODIFIERS.alt)) {
|
||||
return true;
|
||||
}
|
||||
// \x1b\r is alt+enter only in legacy mode (no Kitty protocol)
|
||||
// When Kitty protocol is active, alt+enter comes as CSI u sequence
|
||||
if (!_kittyProtocolActive) {
|
||||
|
|
|
|||
|
|
@ -85,13 +85,34 @@ export class ProcessTerminal implements Terminal {
|
|||
/**
|
||||
* Set up StdinBuffer to split batched input into individual sequences.
|
||||
* This ensures components receive single events, making matchesKey/isKeyRelease work correctly.
|
||||
* Note: Does NOT register the stdin handler - that's done after the Kitty protocol query.
|
||||
*
|
||||
* Also watches for Kitty protocol response and enables it when detected.
|
||||
* This is done here (after stdinBuffer parsing) rather than on raw stdin
|
||||
* to handle the case where the response arrives split across multiple events.
|
||||
*/
|
||||
private setupStdinBuffer(): void {
|
||||
this.stdinBuffer = new StdinBuffer({ timeout: 10 });
|
||||
|
||||
// Kitty protocol response pattern: \x1b[?<flags>u
|
||||
const kittyResponsePattern = /^\x1b\[\?(\d+)u$/;
|
||||
|
||||
// Forward individual sequences to the input handler
|
||||
this.stdinBuffer.on("data", (sequence) => {
|
||||
// Check for Kitty protocol response (only if not already enabled)
|
||||
if (!this._kittyProtocolActive) {
|
||||
const match = sequence.match(kittyResponsePattern);
|
||||
if (match) {
|
||||
this._kittyProtocolActive = true;
|
||||
setKittyProtocolActive(true);
|
||||
|
||||
// Enable Kitty keyboard protocol (push flags)
|
||||
// Flag 1 = disambiguate escape codes
|
||||
// Flag 2 = report event types (press/repeat/release)
|
||||
process.stdout.write("\x1b[>3u");
|
||||
return; // Don't forward protocol response to TUI
|
||||
}
|
||||
}
|
||||
|
||||
if (this.inputHandler) {
|
||||
this.inputHandler(sequence);
|
||||
}
|
||||
|
|
@ -105,7 +126,6 @@ export class ProcessTerminal implements Terminal {
|
|||
});
|
||||
|
||||
// Handler that pipes stdin data through the buffer
|
||||
// Registration happens after Kitty protocol query completes
|
||||
this.stdinDataHandler = (data: string) => {
|
||||
this.stdinBuffer!.process(data);
|
||||
};
|
||||
|
|
@ -117,81 +137,13 @@ export class ProcessTerminal implements Terminal {
|
|||
* Sends CSI ? u to query current flags. If terminal responds with CSI ? <flags> u,
|
||||
* it supports the protocol and we enable it with CSI > 1 u.
|
||||
*
|
||||
* Non-supporting terminals won't respond, so we use a timeout.
|
||||
* The response is detected in setupStdinBuffer's data handler, which properly
|
||||
* handles the case where the response arrives split across multiple stdin events.
|
||||
*/
|
||||
private queryAndEnableKittyProtocol(): void {
|
||||
const QUERY_TIMEOUT_MS = 100;
|
||||
let resolved = false;
|
||||
let buffer = "";
|
||||
|
||||
// Kitty protocol response pattern: \x1b[?<flags>u
|
||||
const kittyResponsePattern = /\x1b\[\?(\d+)u/;
|
||||
|
||||
const queryHandler = (data: string) => {
|
||||
if (resolved) {
|
||||
// Query phase done, forward to StdinBuffer
|
||||
if (this.stdinBuffer) {
|
||||
this.stdinBuffer.process(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += data;
|
||||
|
||||
// Check if we have a Kitty protocol response
|
||||
const match = buffer.match(kittyResponsePattern);
|
||||
if (match) {
|
||||
resolved = true;
|
||||
this._kittyProtocolActive = true;
|
||||
setKittyProtocolActive(true);
|
||||
|
||||
// Enable Kitty keyboard protocol (push flags)
|
||||
// Flag 1 = disambiguate escape codes
|
||||
// Flag 2 = report event types (press/repeat/release)
|
||||
process.stdout.write("\x1b[>3u");
|
||||
|
||||
// Remove the response from buffer, forward any remaining input through StdinBuffer
|
||||
const remaining = buffer.replace(kittyResponsePattern, "");
|
||||
if (remaining && this.stdinBuffer) {
|
||||
this.stdinBuffer.process(remaining);
|
||||
}
|
||||
|
||||
// Replace query handler with StdinBuffer handler
|
||||
process.stdin.removeListener("data", queryHandler);
|
||||
if (this.stdinDataHandler) {
|
||||
process.stdin.on("data", this.stdinDataHandler);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Set up StdinBuffer before query (it will receive input after query completes)
|
||||
this.setupStdinBuffer();
|
||||
|
||||
// Temporarily intercept input for the query (before StdinBuffer)
|
||||
process.stdin.on("data", queryHandler);
|
||||
|
||||
// Send query
|
||||
process.stdin.on("data", this.stdinDataHandler!);
|
||||
process.stdout.write("\x1b[?u");
|
||||
|
||||
// Timeout: if no response, terminal doesn't support Kitty protocol
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
this._kittyProtocolActive = false;
|
||||
setKittyProtocolActive(false);
|
||||
|
||||
// Forward any buffered input that wasn't a Kitty response through StdinBuffer
|
||||
if (buffer && this.stdinBuffer) {
|
||||
this.stdinBuffer.process(buffer);
|
||||
}
|
||||
|
||||
// Replace query handler with StdinBuffer handler
|
||||
process.stdin.removeListener("data", queryHandler);
|
||||
if (this.stdinDataHandler) {
|
||||
process.stdin.on("data", this.stdinDataHandler);
|
||||
}
|
||||
}
|
||||
}, QUERY_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue