mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-18 17:04:49 +00:00
Components with search inputs now implement Focusable interface and propagate focus state to their child Input components. This allows the hardware cursor to be positioned correctly for IME candidate window placement. Affected components: - ModelSelectorComponent - ScopedModelsSelectorComponent - SessionSelectorComponent (and SessionList) - ExtensionInputComponent - LoginDialogComponent - TreeSelectorComponent (and LabelInput) fixes #827
85 lines
2.3 KiB
TypeScript
85 lines
2.3 KiB
TypeScript
/**
|
|
* Simple text input component for extensions.
|
|
*/
|
|
|
|
import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
|
import { theme } from "../theme/theme.js";
|
|
import { CountdownTimer } from "./countdown-timer.js";
|
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
import { keyHint } from "./keybinding-hints.js";
|
|
|
|
export interface ExtensionInputOptions {
|
|
tui?: TUI;
|
|
timeout?: number;
|
|
}
|
|
|
|
export class ExtensionInputComponent extends Container implements Focusable {
|
|
private input: Input;
|
|
private onSubmitCallback: (value: string) => void;
|
|
private onCancelCallback: () => void;
|
|
private titleText: Text;
|
|
private baseTitle: string;
|
|
private countdown: CountdownTimer | undefined;
|
|
|
|
// Focusable implementation - propagate to input for IME cursor positioning
|
|
private _focused = false;
|
|
get focused(): boolean {
|
|
return this._focused;
|
|
}
|
|
set focused(value: boolean) {
|
|
this._focused = value;
|
|
this.input.focused = value;
|
|
}
|
|
|
|
constructor(
|
|
title: string,
|
|
_placeholder: string | undefined,
|
|
onSubmit: (value: string) => void,
|
|
onCancel: () => void,
|
|
opts?: ExtensionInputOptions,
|
|
) {
|
|
super();
|
|
|
|
this.onSubmitCallback = onSubmit;
|
|
this.onCancelCallback = onCancel;
|
|
this.baseTitle = title;
|
|
|
|
this.addChild(new DynamicBorder());
|
|
this.addChild(new Spacer(1));
|
|
|
|
this.titleText = new Text(theme.fg("accent", title), 1, 0);
|
|
this.addChild(this.titleText);
|
|
this.addChild(new Spacer(1));
|
|
|
|
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
this.countdown = new CountdownTimer(
|
|
opts.timeout,
|
|
opts.tui,
|
|
(s) => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)),
|
|
() => this.onCancelCallback(),
|
|
);
|
|
}
|
|
|
|
this.input = new Input();
|
|
this.addChild(this.input);
|
|
this.addChild(new Spacer(1));
|
|
this.addChild(new Text(`${keyHint("selectConfirm", "submit")} ${keyHint("selectCancel", "cancel")}`, 1, 0));
|
|
this.addChild(new Spacer(1));
|
|
this.addChild(new DynamicBorder());
|
|
}
|
|
|
|
handleInput(keyData: string): void {
|
|
const kb = getEditorKeybindings();
|
|
if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
|
|
this.onSubmitCallback(this.input.getValue());
|
|
} else if (kb.matches(keyData, "selectCancel")) {
|
|
this.onCancelCallback();
|
|
} else {
|
|
this.input.handleInput(keyData);
|
|
}
|
|
}
|
|
|
|
dispose(): void {
|
|
this.countdown?.dispose();
|
|
}
|
|
}
|