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
This commit is contained in:
Mario Zechner 2025-12-19 21:53:04 +01:00
parent 65c292b7dd
commit 8d1229b5ec
6 changed files with 45 additions and 21 deletions

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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);