From 8d1229b5ec7c4dffa21eebc79d52915eda8f7e5e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 19 Dec 2025 21:53:04 +0100 Subject: [PATCH] fix(coding-agent): use key helpers for arrow keys and Enter Fixed arrow key and Enter detection in selector components to work with Kitty protocol when Caps Lock or Num Lock is enabled. Updated: oauth-selector, user-message-selector, hook-selector, hook-input, model-selector, session-selector --- .../interactive/components/hook-input.ts | 4 ++-- .../interactive/components/hook-selector.ts | 8 ++++---- .../interactive/components/model-selector.ts | 18 ++++++++++++++---- .../interactive/components/oauth-selector.ts | 8 ++++---- .../components/session-selector.ts | 9 ++++++--- .../components/user-message-selector.ts | 19 +++++++++++++++---- 6 files changed, 45 insertions(+), 21 deletions(-) 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 7b370477..a7b30ba0 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, isEscape, Spacer, Text } from "@mariozechner/pi-tui"; +import { Container, Input, isEnter, isEscape, Spacer, Text } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -47,7 +47,7 @@ export class HookInputComponent extends Container { handleInput(keyData: string): void { // Enter - if (keyData === "\r" || keyData === "\n") { + if (isEnter(keyData) || keyData === "\n") { this.onSubmitCallback(this.input.getValue()); 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 93d95e64..cc00b299 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, isEscape, Spacer, Text } from "@mariozechner/pi-tui"; +import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text } from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -67,17 +67,17 @@ export class HookSelectorComponent extends Container { handleInput(keyData: string): void { // Up arrow or k - if (keyData === "\x1b[A" || keyData === "k") { + if (isArrowUp(keyData) || keyData === "k") { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.updateList(); } // Down arrow or j - else if (keyData === "\x1b[B" || keyData === "j") { + else if (isArrowDown(keyData) || keyData === "j") { this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1); this.updateList(); } // Enter - else if (keyData === "\r" || keyData === "\n") { + else if (isEnter(keyData) || keyData === "\n") { const selected = this.options[this.selectedIndex]; if (selected) { this.onSelectCallback(selected); 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 f2588fba..4c85bd8a 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,15 @@ import type { Model } from "@mariozechner/pi-ai"; -import { Container, Input, isEscape, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; +import { + Container, + Input, + isArrowDown, + isArrowUp, + isEnter, + 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"; @@ -175,17 +185,17 @@ export class ModelSelectorComponent extends Container { handleInput(keyData: string): void { // Up arrow - wrap to bottom when at top - if (keyData === "\x1b[A") { + if (isArrowUp(keyData)) { this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1; this.updateList(); } // Down arrow - wrap to top when at bottom - else if (keyData === "\x1b[B") { + else if (isArrowDown(keyData)) { this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1; this.updateList(); } // Enter - else if (keyData === "\r") { + else if (isEnter(keyData)) { const selectedModel = this.filteredModels[this.selectedIndex]; if (selectedModel) { this.handleSelect(selectedModel.model); 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 e2b5b934..61a7b73a 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, isEscape, Spacer, TruncatedText } from "@mariozechner/pi-tui"; +import { Container, isArrowDown, isArrowUp, isEnter, 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"; @@ -90,17 +90,17 @@ export class OAuthSelectorComponent extends Container { handleInput(keyData: string): void { // Up arrow - if (keyData === "\x1b[A") { + if (isArrowUp(keyData)) { this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.updateList(); } // Down arrow - else if (keyData === "\x1b[B") { + else if (isArrowDown(keyData)) { this.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1); this.updateList(); } // Enter - else if (keyData === "\r") { + else if (isEnter(keyData)) { const selectedProvider = this.allProviders[this.selectedIndex]; if (selectedProvider?.available) { this.onSelectCallback(selectedProvider.id); 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 de34155a..678cbf20 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -2,7 +2,10 @@ import { type Component, Container, Input, + isArrowDown, + isArrowUp, isCtrlC, + isEnter, isEscape, Spacer, Text, @@ -134,15 +137,15 @@ class SessionList implements Component { handleInput(keyData: string): void { // Up arrow - if (keyData === "\x1b[A") { + if (isArrowUp(keyData)) { this.selectedIndex = Math.max(0, this.selectedIndex - 1); } // Down arrow - else if (keyData === "\x1b[B") { + else if (isArrowDown(keyData)) { this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1); } // Enter - else if (keyData === "\r") { + else if (isEnter(keyData)) { const selected = this.filteredSessions[this.selectedIndex]; if (selected && this.onSelect) { this.onSelect(selected.path); 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 a3471ca8..18cd769c 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,15 @@ -import { type Component, Container, isCtrlC, isEscape, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"; +import { + type Component, + Container, + isArrowDown, + isArrowUp, + isCtrlC, + isEnter, + isEscape, + Spacer, + Text, + truncateToWidth, +} from "@mariozechner/pi-tui"; import { theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -79,15 +90,15 @@ class UserMessageList implements Component { handleInput(keyData: string): void { // Up arrow - go to previous (older) message, wrap to bottom when at top - if (keyData === "\x1b[A") { + if (isArrowUp(keyData)) { this.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1; } // Down arrow - go to next (newer) message, wrap to top when at bottom - else if (keyData === "\x1b[B") { + else if (isArrowDown(keyData)) { this.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1; } // Enter - select message and branch - else if (keyData === "\r") { + else if (isEnter(keyData)) { const selected = this.messages[this.selectedIndex]; if (selected && this.onSelect) { this.onSelect(selected.index);