Files
sproutclaw/packages/coding-agent/src/modes/interactive/components/hook-selector.ts
Mario Zechner 8d1229b5ec 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
2025-12-19 21:53:04 +01:00

92 lines
2.4 KiB
TypeScript

/**
* Generic selector component for hooks.
* Displays a list of string options with keyboard navigation.
*/
import { Container, isArrowDown, isArrowUp, isEnter, isEscape, Spacer, Text } from "@mariozechner/pi-tui";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
export class HookSelectorComponent extends Container {
private options: string[];
private selectedIndex = 0;
private listContainer: Container;
private onSelectCallback: (option: string) => void;
private onCancelCallback: () => void;
constructor(title: string, options: string[], onSelect: (option: string) => void, onCancel: () => void) {
super();
this.options = options;
this.onSelectCallback = onSelect;
this.onCancelCallback = onCancel;
// Add top border
this.addChild(new DynamicBorder());
this.addChild(new Spacer(1));
// Add title
this.addChild(new Text(theme.fg("accent", title), 1, 0));
this.addChild(new Spacer(1));
// Create list container
this.listContainer = new Container();
this.addChild(this.listContainer);
this.addChild(new Spacer(1));
// Add hint
this.addChild(new Text(theme.fg("dim", "↑↓ navigate enter select esc cancel"), 1, 0));
this.addChild(new Spacer(1));
// Add bottom border
this.addChild(new DynamicBorder());
// Initial render
this.updateList();
}
private updateList(): void {
this.listContainer.clear();
for (let i = 0; i < this.options.length; i++) {
const option = this.options[i];
const isSelected = i === this.selectedIndex;
let text = "";
if (isSelected) {
text = theme.fg("accent", "→ ") + theme.fg("accent", option);
} else {
text = " " + theme.fg("text", option);
}
this.listContainer.addChild(new Text(text, 1, 0));
}
}
handleInput(keyData: string): void {
// Up arrow or k
if (isArrowUp(keyData) || keyData === "k") {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
this.updateList();
}
// Down arrow or j
else if (isArrowDown(keyData) || keyData === "j") {
this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
this.updateList();
}
// Enter
else if (isEnter(keyData) || keyData === "\n") {
const selected = this.options[this.selectedIndex];
if (selected) {
this.onSelectCallback(selected);
}
}
// Escape
else if (isEscape(keyData)) {
this.onCancelCallback();
}
}
}