From f8b6164ecdc2b88540b4abc41f8454dd03297c27 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 19 Dec 2025 02:09:51 +0100 Subject: [PATCH] Fix Escape key handling for Kitty keyboard protocol Add isEscape() helper that handles both raw (\x1b) and Kitty protocol (\x1b[27u) Escape sequences. Update all components that check for Escape key to use the new helper. --- .../modes/interactive/components/custom-editor.ts | 4 ++-- .../src/modes/interactive/components/hook-input.ts | 4 ++-- .../modes/interactive/components/hook-selector.ts | 4 ++-- .../modes/interactive/components/model-selector.ts | 4 ++-- .../modes/interactive/components/oauth-selector.ts | 4 ++-- .../interactive/components/session-selector.ts | 13 +++++++++++-- .../interactive/components/user-message-selector.ts | 4 ++-- packages/tui/src/components/editor.ts | 4 ++-- packages/tui/src/components/select-list.ts | 4 ++-- packages/tui/src/index.ts | 1 + packages/tui/src/keys.ts | 10 ++++++++++ 11 files changed, 38 insertions(+), 18 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 92b8ab0c..98bbfd19 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -1,4 +1,4 @@ -import { Editor, isCtrlC, isCtrlD, isCtrlO, isCtrlP, isCtrlT, isShiftTab } from "@mariozechner/pi-tui"; +import { Editor, isCtrlC, isCtrlD, isCtrlO, isCtrlP, isCtrlT, isEscape, isShiftTab } from "@mariozechner/pi-tui"; /** * Custom editor that handles Escape and Ctrl+C keys for coding-agent @@ -39,7 +39,7 @@ export class CustomEditor extends Editor { // Intercept Escape key - but only if autocomplete is NOT active // (let parent handle escape for autocomplete cancellation) - if (data === "\x1b" && this.onEscape && !this.isShowingAutocomplete()) { + if (isEscape(data) && this.onEscape && !this.isShowingAutocomplete()) { this.onEscape(); return; } diff --git a/packages/coding-agent/src/modes/interactive/components/hook-input.ts b/packages/coding-agent/src/modes/interactive/components/hook-input.ts index de7d7b9d..7b370477 100644 --- a/packages/coding-agent/src/modes/interactive/components/hook-input.ts +++ b/packages/coding-agent/src/modes/interactive/components/hook-input.ts @@ -2,7 +2,7 @@ * Simple text input component for hooks. */ -import { Container, Input, Spacer, Text } from "@mariozechner/pi-tui"; +import { Container, Input, isEscape, Spacer, Text } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -53,7 +53,7 @@ export class HookInputComponent extends Container { } // Escape to cancel - if (keyData === "\x1b") { + if (isEscape(keyData)) { this.onCancelCallback(); return; } diff --git a/packages/coding-agent/src/modes/interactive/components/hook-selector.ts b/packages/coding-agent/src/modes/interactive/components/hook-selector.ts index 9936b756..93d95e64 100644 --- a/packages/coding-agent/src/modes/interactive/components/hook-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/hook-selector.ts @@ -3,7 +3,7 @@ * Displays a list of string options with keyboard navigation. */ -import { Container, Spacer, Text } from "@mariozechner/pi-tui"; +import { Container, isEscape, Spacer, Text } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -84,7 +84,7 @@ export class HookSelectorComponent extends Container { } } // Escape - else if (keyData === "\x1b") { + else if (isEscape(keyData)) { this.onCancelCallback(); } } diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index 17afdbc7..f2588fba 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -1,5 +1,5 @@ import type { Model } from "@mariozechner/pi-ai"; -import { Container, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; +import { Container, Input, isEscape, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; import { getAvailableModels } from "../../../core/model-config.js"; import type { SettingsManager } from "../../../core/settings-manager.js"; import { fuzzyFilter } from "../../../utils/fuzzy.js"; @@ -192,7 +192,7 @@ export class ModelSelectorComponent extends Container { } } // Escape - else if (keyData === "\x1b") { + else if (isEscape(keyData)) { this.onCancelCallback(); } // Pass everything else to search input diff --git a/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts b/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts index 2d062e6f..e2b5b934 100644 --- a/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts @@ -1,4 +1,4 @@ -import { Container, Spacer, TruncatedText } from "@mariozechner/pi-tui"; +import { Container, isEscape, Spacer, TruncatedText } from "@mariozechner/pi-tui"; import { getOAuthProviders, type OAuthProviderInfo } from "../../../core/oauth/index.js"; import { loadOAuthCredentials } from "../../../core/oauth/storage.js"; import { theme } from "../theme/theme.js"; @@ -107,7 +107,7 @@ export class OAuthSelectorComponent extends Container { } } // Escape - else if (keyData === "\x1b") { + else if (isEscape(keyData)) { this.onCancelCallback(); } } diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index cbc96696..de3c43de 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -1,4 +1,13 @@ -import { type Component, Container, Input, isCtrlC, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"; +import { + type Component, + Container, + Input, + isCtrlC, + isEscape, + Spacer, + Text, + truncateToWidth, +} from "@mariozechner/pi-tui"; import type { SessionManager } from "../../../core/session-manager.js"; import { fuzzyFilter } from "../../../utils/fuzzy.js"; import { theme } from "../theme/theme.js"; @@ -139,7 +148,7 @@ class SessionList implements Component { } } // Escape - cancel - else if (keyData === "\x1b") { + else if (isEscape(keyData)) { if (this.onCancel) { this.onCancel(); } diff --git a/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts b/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts index d66a8ca1..a3471ca8 100644 --- a/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts @@ -1,4 +1,4 @@ -import { type Component, Container, isCtrlC, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"; +import { type Component, Container, isCtrlC, isEscape, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -94,7 +94,7 @@ class UserMessageList implements Component { } } // Escape - cancel - else if (keyData === "\x1b") { + else if (isEscape(keyData)) { if (this.onCancel) { this.onCancel(); } diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index da56d285..c4a21968 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -1,5 +1,5 @@ import type { AutocompleteProvider, CombinedAutocompleteProvider } from "../autocomplete.js"; -import { isAltBackspace, isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlU, isCtrlW, Keys } from "../keys.js"; +import { isAltBackspace, isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlU, isCtrlW, isEscape, Keys } from "../keys.js"; import type { Component } from "../tui.js"; import { visibleWidth } from "../utils.js"; import { SelectList, type SelectListTheme } from "./select-list.js"; @@ -267,7 +267,7 @@ export class Editor implements Component { // Handle autocomplete special keys first (but don't block other input) if (this.isAutocompleting && this.autocompleteList) { // Escape - cancel autocomplete - if (data === "\x1b") { + if (isEscape(data)) { this.cancelAutocomplete(); return; } diff --git a/packages/tui/src/components/select-list.ts b/packages/tui/src/components/select-list.ts index ab2f5e33..000b8191 100644 --- a/packages/tui/src/components/select-list.ts +++ b/packages/tui/src/components/select-list.ts @@ -1,4 +1,4 @@ -import { isCtrlC } from "../keys.js"; +import { isCtrlC, isEscape } from "../keys.js"; import type { Component } from "../tui.js"; import { truncateToWidth } from "../utils.js"; @@ -163,7 +163,7 @@ export class SelectList implements Component { } } // Escape or Ctrl+C - else if (keyData === "\x1b" || isCtrlC(keyData)) { + else if (isEscape(keyData) || isCtrlC(keyData)) { if (this.onCancel) { this.onCancel(); } diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index b03c0cf3..26c15600 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -31,6 +31,7 @@ export { isCtrlT, isCtrlU, isCtrlW, + isEscape, isShiftTab, Keys, } from "./keys.js"; diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index bac4557d..b1382f9e 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -28,6 +28,7 @@ const CODEPOINTS = { w: 119, // Special keys + escape: 27, tab: 9, enter: 13, backspace: 127, @@ -194,3 +195,12 @@ export function isAltBackspace(data: string): boolean { export function isShiftTab(data: string): boolean { return data === RAW.SHIFT_TAB || data === Keys.SHIFT_TAB; } + +/** + * Check if input matches the Escape key (raw byte or Kitty protocol). + * Raw: \x1b (single byte) + * Kitty: \x1b[27u (codepoint 27 = escape) + */ +export function isEscape(data: string): boolean { + return data === "\x1b" || data === `\x1b[${CODEPOINTS.escape}u`; +}