fix(keybindings): migrate to namespaced ids closes #2391
This commit is contained in:
@@ -12,7 +12,7 @@ import {
|
||||
} from "../../../core/tools/truncate.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { editorKey, keyHint } from "./keybinding-hints.js";
|
||||
import { keyHint, keyText } from "./keybinding-hints.js";
|
||||
import { truncateToVisualLines } from "./visual-truncate.js";
|
||||
|
||||
// Preview line limit when not expanded (matches tool execution behavior)
|
||||
@@ -58,7 +58,7 @@ export class BashExecutionComponent extends Container {
|
||||
ui,
|
||||
(spinner) => theme.fg(colorKey, spinner),
|
||||
(text) => theme.fg("muted", text),
|
||||
`Running... (${editorKey("selectCancel")} to cancel)`, // Plain text for loader
|
||||
`Running... (${keyText("tui.select.cancel")} to cancel)`, // Plain text for loader
|
||||
);
|
||||
this.contentContainer.addChild(this.loader);
|
||||
|
||||
@@ -168,10 +168,10 @@ export class BashExecutionComponent extends Container {
|
||||
// Show how many lines are hidden (collapsed preview)
|
||||
if (hiddenLineCount > 0) {
|
||||
if (this.expanded) {
|
||||
statusParts.push(`(${keyHint("expandTools", "to collapse")})`);
|
||||
statusParts.push(`(${keyHint("app.tools.expand", "to collapse")})`);
|
||||
} else {
|
||||
statusParts.push(
|
||||
`${theme.fg("muted", `... ${hiddenLineCount} more lines`)} (${keyHint("expandTools", "to expand")})`,
|
||||
`${theme.fg("muted", `... ${hiddenLineCount} more lines`)} (${keyHint("app.tools.expand", "to expand")})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class BorderedLoader extends Container {
|
||||
this.addChild(this.loader);
|
||||
if (this.cancellable) {
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(keyHint("selectCancel", "cancel"), 1, 0));
|
||||
this.addChild(new Text(keyHint("tui.select.cancel", "cancel"), 1, 0));
|
||||
}
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new DynamicBorder(borderColor));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import type { BranchSummaryMessage } from "../../../core/messages.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { editorKey } from "./keybinding-hints.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
|
||||
/**
|
||||
* Component that renders a branch summary message with collapsed/expanded state.
|
||||
@@ -47,7 +47,7 @@ export class BranchSummaryMessageComponent extends Box {
|
||||
this.addChild(
|
||||
new Text(
|
||||
theme.fg("customMessageText", "Branch summary (") +
|
||||
theme.fg("dim", editorKey("expandTools")) +
|
||||
theme.fg("dim", keyText("app.tools.expand")) +
|
||||
theme.fg("customMessageText", " to expand)"),
|
||||
0,
|
||||
0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Markdown, type MarkdownTheme, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import type { CompactionSummaryMessage } from "../../../core/messages.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { editorKey } from "./keybinding-hints.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
|
||||
/**
|
||||
* Component that renders a compaction message with collapsed/expanded state.
|
||||
@@ -48,7 +48,7 @@ export class CompactionSummaryMessageComponent extends Box {
|
||||
this.addChild(
|
||||
new Text(
|
||||
theme.fg("customMessageText", `Compacted from ${tokenStr} tokens (`) +
|
||||
theme.fg("dim", editorKey("expandTools")) +
|
||||
theme.fg("dim", keyText("app.tools.expand")) +
|
||||
theme.fg("customMessageText", " to expand)"),
|
||||
0,
|
||||
0,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type Component,
|
||||
Container,
|
||||
type Focusable,
|
||||
getEditorKeybindings,
|
||||
getKeybindings,
|
||||
Input,
|
||||
matchesKey,
|
||||
Spacer,
|
||||
@@ -355,17 +355,17 @@ class ResourceList implements Component, Focusable {
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
|
||||
if (kb.matches(data, "selectUp")) {
|
||||
if (kb.matches(data, "tui.select.up")) {
|
||||
this.selectedIndex = this.findNextItem(this.selectedIndex, -1);
|
||||
return;
|
||||
}
|
||||
if (kb.matches(data, "selectDown")) {
|
||||
if (kb.matches(data, "tui.select.down")) {
|
||||
this.selectedIndex = this.findNextItem(this.selectedIndex, 1);
|
||||
return;
|
||||
}
|
||||
if (kb.matches(data, "selectPageUp")) {
|
||||
if (kb.matches(data, "tui.select.pageUp")) {
|
||||
// Jump up by maxVisible, then find nearest item
|
||||
let target = Math.max(0, this.selectedIndex - this.maxVisible);
|
||||
while (target < this.filteredItems.length && this.filteredItems[target].type !== "item") {
|
||||
@@ -376,7 +376,7 @@ class ResourceList implements Component, Focusable {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kb.matches(data, "selectPageDown")) {
|
||||
if (kb.matches(data, "tui.select.pageDown")) {
|
||||
// Jump down by maxVisible, then find nearest item
|
||||
let target = Math.min(this.filteredItems.length - 1, this.selectedIndex + this.maxVisible);
|
||||
while (target >= 0 && this.filteredItems[target].type !== "item") {
|
||||
@@ -387,7 +387,7 @@ class ResourceList implements Component, Focusable {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (kb.matches(data, "selectCancel")) {
|
||||
if (kb.matches(data, "tui.select.cancel")) {
|
||||
this.onCancel?.();
|
||||
return;
|
||||
}
|
||||
@@ -395,7 +395,7 @@ class ResourceList implements Component, Focusable {
|
||||
this.onExit?.();
|
||||
return;
|
||||
}
|
||||
if (data === " " || kb.matches(data, "selectConfirm")) {
|
||||
if (data === " " || kb.matches(data, "tui.select.confirm")) {
|
||||
const entry = this.filteredItems[this.selectedIndex];
|
||||
if (entry?.type === "item") {
|
||||
const newEnabled = !entry.item.enabled;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Editor, type EditorOptions, type EditorTheme, type TUI } from "@mariozechner/pi-tui";
|
||||
import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import type { AppKeybinding, KeybindingsManager } from "../../../core/keybindings.js";
|
||||
|
||||
/**
|
||||
* Custom editor that handles app-level keybindings for coding-agent.
|
||||
*/
|
||||
export class CustomEditor extends Editor {
|
||||
private keybindings: KeybindingsManager;
|
||||
public actionHandlers: Map<AppAction, () => void> = new Map();
|
||||
public actionHandlers: Map<AppKeybinding, () => void> = new Map();
|
||||
|
||||
// Special handlers that can be dynamically replaced
|
||||
public onEscape?: () => void;
|
||||
@@ -23,7 +23,7 @@ export class CustomEditor extends Editor {
|
||||
/**
|
||||
* Register a handler for an app action.
|
||||
*/
|
||||
onAction(action: AppAction, handler: () => void): void {
|
||||
onAction(action: AppKeybinding, handler: () => void): void {
|
||||
this.actionHandlers.set(action, handler);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class CustomEditor extends Editor {
|
||||
}
|
||||
|
||||
// Check for paste image keybinding
|
||||
if (this.keybindings.matches(data, "pasteImage")) {
|
||||
if (this.keybindings.matches(data, "app.clipboard.pasteImage")) {
|
||||
this.onPasteImage?.();
|
||||
return;
|
||||
}
|
||||
@@ -42,10 +42,10 @@ export class CustomEditor extends Editor {
|
||||
// Check app keybindings first
|
||||
|
||||
// Escape/interrupt - only if autocomplete is NOT active
|
||||
if (this.keybindings.matches(data, "interrupt")) {
|
||||
if (this.keybindings.matches(data, "app.interrupt")) {
|
||||
if (!this.isShowingAutocomplete()) {
|
||||
// Use dynamic onEscape if set, otherwise registered handler
|
||||
const handler = this.onEscape ?? this.actionHandlers.get("interrupt");
|
||||
const handler = this.onEscape ?? this.actionHandlers.get("app.interrupt");
|
||||
if (handler) {
|
||||
handler();
|
||||
return;
|
||||
@@ -57,9 +57,9 @@ export class CustomEditor extends Editor {
|
||||
}
|
||||
|
||||
// Exit (Ctrl+D) - only when editor is empty
|
||||
if (this.keybindings.matches(data, "exit")) {
|
||||
if (this.keybindings.matches(data, "app.exit")) {
|
||||
if (this.getText().length === 0) {
|
||||
const handler = this.onCtrlD ?? this.actionHandlers.get("exit");
|
||||
const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit");
|
||||
if (handler) handler();
|
||||
return;
|
||||
}
|
||||
@@ -68,7 +68,7 @@ export class CustomEditor extends Editor {
|
||||
|
||||
// Check all other app actions
|
||||
for (const [action, handler] of this.actionHandlers) {
|
||||
if (action !== "interrupt" && action !== "exit" && this.keybindings.matches(data, action)) {
|
||||
if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) {
|
||||
handler();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Editor,
|
||||
type EditorOptions,
|
||||
type Focusable,
|
||||
getEditorKeybindings,
|
||||
getKeybindings,
|
||||
Spacer,
|
||||
Text,
|
||||
type TUI,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import type { KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import { getEditorTheme, theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { appKeyHint, keyHint } from "./keybinding-hints.js";
|
||||
import { keyHint } from "./keybinding-hints.js";
|
||||
|
||||
export class ExtensionEditorComponent extends Container implements Focusable {
|
||||
private editor: Editor;
|
||||
@@ -78,12 +78,12 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
||||
// Add hint
|
||||
const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR);
|
||||
const hint =
|
||||
keyHint("selectConfirm", "submit") +
|
||||
keyHint("tui.select.confirm", "submit") +
|
||||
" " +
|
||||
keyHint("newLine", "newline") +
|
||||
keyHint("tui.input.newLine", "newline") +
|
||||
" " +
|
||||
keyHint("selectCancel", "cancel") +
|
||||
(hasExternalEditor ? ` ${appKeyHint(this.keybindings, "externalEditor", "external editor")}` : "");
|
||||
keyHint("tui.select.cancel", "cancel") +
|
||||
(hasExternalEditor ? ` ${keyHint("app.editor.external", "external editor")}` : "");
|
||||
this.addChild(new Text(hint, 1, 0));
|
||||
|
||||
this.addChild(new Spacer(1));
|
||||
@@ -93,15 +93,15 @@ export class ExtensionEditorComponent extends Container implements Focusable {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
// Escape or Ctrl+C to cancel
|
||||
if (kb.matches(keyData, "selectCancel")) {
|
||||
if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancelCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
// External editor (app keybinding)
|
||||
if (this.keybindings.matches(keyData, "externalEditor")) {
|
||||
if (this.keybindings.matches(keyData, "app.editor.external")) {
|
||||
this.openExternalEditor();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Simple text input component for extensions.
|
||||
*/
|
||||
|
||||
import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import { Container, type Focusable, getKeybindings, 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";
|
||||
@@ -63,16 +63,18 @@ export class ExtensionInputComponent extends Container implements Focusable {
|
||||
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 Text(`${keyHint("tui.select.confirm", "submit")} ${keyHint("tui.select.cancel", "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") {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n") {
|
||||
this.onSubmitCallback(this.input.getValue());
|
||||
} else if (kb.matches(keyData, "selectCancel")) {
|
||||
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancelCallback();
|
||||
} else {
|
||||
this.input.handleInput(keyData);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Displays a list of string options with keyboard navigation.
|
||||
*/
|
||||
|
||||
import { Container, getEditorKeybindings, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import { Container, getKeybindings, 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";
|
||||
@@ -61,9 +61,9 @@ export class ExtensionSelectorComponent extends Container {
|
||||
new Text(
|
||||
rawKeyHint("↑↓", "navigate") +
|
||||
" " +
|
||||
keyHint("selectConfirm", "select") +
|
||||
keyHint("tui.select.confirm", "select") +
|
||||
" " +
|
||||
keyHint("selectCancel", "cancel"),
|
||||
keyHint("tui.select.cancel", "cancel"),
|
||||
1,
|
||||
0,
|
||||
),
|
||||
@@ -86,17 +86,17 @@ export class ExtensionSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
if (kb.matches(keyData, "selectUp") || keyData === "k") {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "tui.select.up") || keyData === "k") {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.updateList();
|
||||
} else if (kb.matches(keyData, "selectDown") || keyData === "j") {
|
||||
} else if (kb.matches(keyData, "tui.select.down") || keyData === "j") {
|
||||
this.selectedIndex = Math.min(this.options.length - 1, this.selectedIndex + 1);
|
||||
this.updateList();
|
||||
} else if (kb.matches(keyData, "selectConfirm") || keyData === "\n") {
|
||||
} else if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n") {
|
||||
const selected = this.options[this.selectedIndex];
|
||||
if (selected) this.onSelectCallback(selected);
|
||||
} else if (kb.matches(keyData, "selectCancel")) {
|
||||
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancelCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export { ExtensionEditorComponent } from "./extension-editor.js";
|
||||
export { ExtensionInputComponent } from "./extension-input.js";
|
||||
export { ExtensionSelectorComponent } from "./extension-selector.js";
|
||||
export { FooterComponent } from "./footer.js";
|
||||
export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding-hints.js";
|
||||
export { keyHint, keyText, rawKeyHint } from "./keybinding-hints.js";
|
||||
export { LoginDialogComponent } from "./login-dialog.js";
|
||||
export { ModelSelectorComponent } from "./model-selector.js";
|
||||
export { OAuthSelectorComponent } from "./oauth-selector.js";
|
||||
|
||||
@@ -2,65 +2,23 @@
|
||||
* Utilities for formatting keybinding hints in the UI.
|
||||
*/
|
||||
|
||||
import { type EditorAction, getEditorKeybindings, type KeyId } from "@mariozechner/pi-tui";
|
||||
import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import { getKeybindings, type Keybinding, type KeyId } from "@mariozechner/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
|
||||
/**
|
||||
* Format keys array as display string (e.g., ["ctrl+c", "escape"] -> "ctrl+c/escape").
|
||||
*/
|
||||
function formatKeys(keys: KeyId[]): string {
|
||||
if (keys.length === 0) return "";
|
||||
if (keys.length === 1) return keys[0]!;
|
||||
return keys.join("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display string for an editor action.
|
||||
*/
|
||||
export function editorKey(action: EditorAction): string {
|
||||
return formatKeys(getEditorKeybindings().getKeys(action));
|
||||
export function keyText(keybinding: Keybinding): string {
|
||||
return formatKeys(getKeybindings().getKeys(keybinding));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display string for an app action.
|
||||
*/
|
||||
export function appKey(keybindings: KeybindingsManager, action: AppAction): string {
|
||||
return formatKeys(keybindings.getKeys(action));
|
||||
export function keyHint(keybinding: Keybinding, description: string): string {
|
||||
return theme.fg("dim", keyText(keybinding)) + theme.fg("muted", ` ${description}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a keybinding hint with consistent styling: dim key, muted description.
|
||||
* Looks up the key from editor keybindings automatically.
|
||||
*
|
||||
* @param action - Editor action name (e.g., "selectConfirm", "expandTools")
|
||||
* @param description - Description text (e.g., "to expand", "cancel")
|
||||
* @returns Formatted string with dim key and muted description
|
||||
*/
|
||||
export function keyHint(action: EditorAction, description: string): string {
|
||||
return theme.fg("dim", editorKey(action)) + theme.fg("muted", ` ${description}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a keybinding hint for app-level actions.
|
||||
* Requires the KeybindingsManager instance.
|
||||
*
|
||||
* @param keybindings - KeybindingsManager instance
|
||||
* @param action - App action name (e.g., "interrupt", "externalEditor")
|
||||
* @param description - Description text
|
||||
* @returns Formatted string with dim key and muted description
|
||||
*/
|
||||
export function appKeyHint(keybindings: KeybindingsManager, action: AppAction, description: string): string {
|
||||
return theme.fg("dim", appKey(keybindings, action)) + theme.fg("muted", ` ${description}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a raw key string with description (for non-configurable keys like ↑↓).
|
||||
*
|
||||
* @param key - Raw key string
|
||||
* @param description - Description text
|
||||
* @returns Formatted string with dim key and muted description
|
||||
*/
|
||||
export function rawKeyHint(key: string, description: string): string {
|
||||
return theme.fg("dim", key) + theme.fg("muted", ` ${description}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
||||
import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import { exec } from "child_process";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
@@ -109,7 +109,7 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
|
||||
this.contentContainer.addChild(this.input);
|
||||
this.contentContainer.addChild(new Text(`(${keyHint("selectCancel", "to cancel")})`, 1, 0));
|
||||
this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to cancel")})`, 1, 0));
|
||||
this.tui.requestRender();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -130,7 +130,11 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
}
|
||||
this.contentContainer.addChild(this.input);
|
||||
this.contentContainer.addChild(
|
||||
new Text(`(${keyHint("selectCancel", "to cancel,")} ${keyHint("selectConfirm", "to submit")})`, 1, 0),
|
||||
new Text(
|
||||
`(${keyHint("tui.select.cancel", "to cancel,")} ${keyHint("tui.select.confirm", "to submit")})`,
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
this.input.setValue("");
|
||||
@@ -148,7 +152,7 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
showWaiting(message: string): void {
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0));
|
||||
this.contentContainer.addChild(new Text(`(${keyHint("selectCancel", "to cancel")})`, 1, 0));
|
||||
this.contentContainer.addChild(new Text(`(${keyHint("tui.select.cancel", "to cancel")})`, 1, 0));
|
||||
this.tui.requestRender();
|
||||
}
|
||||
|
||||
@@ -161,9 +165,9 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
|
||||
if (kb.matches(data, "selectCancel")) {
|
||||
if (kb.matches(data, "tui.select.cancel")) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Container,
|
||||
type Focusable,
|
||||
fuzzyFilter,
|
||||
getEditorKeybindings,
|
||||
getKeybindings,
|
||||
Input,
|
||||
Spacer,
|
||||
Text,
|
||||
@@ -200,7 +200,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
||||
}
|
||||
|
||||
private getScopeHintText(): string {
|
||||
return keyHint("tab", "scope") + theme.fg("muted", " (all/scoped)");
|
||||
return keyHint("tui.input.tab", "scope") + theme.fg("muted", " (all/scoped)");
|
||||
}
|
||||
|
||||
private setScope(scope: ModelScope): void {
|
||||
@@ -284,8 +284,8 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
if (kb.matches(keyData, "tab")) {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "tui.input.tab")) {
|
||||
if (this.scopedModelItems.length > 0) {
|
||||
const nextScope: ModelScope = this.scope === "all" ? "scoped" : "all";
|
||||
this.setScope(nextScope);
|
||||
@@ -296,26 +296,26 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
||||
return;
|
||||
}
|
||||
// Up arrow - wrap to bottom when at top
|
||||
if (kb.matches(keyData, "selectUp")) {
|
||||
if (kb.matches(keyData, "tui.select.up")) {
|
||||
if (this.filteredModels.length === 0) return;
|
||||
this.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;
|
||||
this.updateList();
|
||||
}
|
||||
// Down arrow - wrap to top when at bottom
|
||||
else if (kb.matches(keyData, "selectDown")) {
|
||||
else if (kb.matches(keyData, "tui.select.down")) {
|
||||
if (this.filteredModels.length === 0) return;
|
||||
this.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
this.updateList();
|
||||
}
|
||||
// Enter
|
||||
else if (kb.matches(keyData, "selectConfirm")) {
|
||||
else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const selectedModel = this.filteredModels[this.selectedIndex];
|
||||
if (selectedModel) {
|
||||
this.handleSelect(selectedModel.model);
|
||||
}
|
||||
}
|
||||
// Escape or Ctrl+C
|
||||
else if (kb.matches(keyData, "selectCancel")) {
|
||||
else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancelCallback();
|
||||
}
|
||||
// Pass everything else to search input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { OAuthProviderInterface } from "@mariozechner/pi-ai";
|
||||
import { getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
||||
import { Container, getEditorKeybindings, Spacer, TruncatedText } from "@mariozechner/pi-tui";
|
||||
import { Container, getKeybindings, Spacer, TruncatedText } from "@mariozechner/pi-tui";
|
||||
import type { AuthStorage } from "../../../core/auth-storage.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
@@ -95,26 +95,26 @@ export class OAuthSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
// Up arrow
|
||||
if (kb.matches(keyData, "selectUp")) {
|
||||
if (kb.matches(keyData, "tui.select.up")) {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
this.updateList();
|
||||
}
|
||||
// Down arrow
|
||||
else if (kb.matches(keyData, "selectDown")) {
|
||||
else if (kb.matches(keyData, "tui.select.down")) {
|
||||
this.selectedIndex = Math.min(this.allProviders.length - 1, this.selectedIndex + 1);
|
||||
this.updateList();
|
||||
}
|
||||
// Enter
|
||||
else if (kb.matches(keyData, "selectConfirm")) {
|
||||
else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const selectedProvider = this.allProviders[this.selectedIndex];
|
||||
if (selectedProvider) {
|
||||
this.onSelectCallback(selectedProvider.id);
|
||||
}
|
||||
}
|
||||
// Escape or Ctrl+C
|
||||
else if (kb.matches(keyData, "selectCancel")) {
|
||||
else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancelCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Container,
|
||||
type Focusable,
|
||||
fuzzyFilter,
|
||||
getEditorKeybindings,
|
||||
getKeybindings,
|
||||
Input,
|
||||
Key,
|
||||
matchesKey,
|
||||
@@ -224,16 +224,16 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
|
||||
// Navigation
|
||||
if (kb.matches(data, "selectUp")) {
|
||||
if (kb.matches(data, "tui.select.up")) {
|
||||
if (this.filteredItems.length === 0) return;
|
||||
this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1;
|
||||
this.updateList();
|
||||
return;
|
||||
}
|
||||
if (kb.matches(data, "selectDown")) {
|
||||
if (kb.matches(data, "tui.select.down")) {
|
||||
if (this.filteredItems.length === 0) return;
|
||||
this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
this.updateList();
|
||||
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
type Component,
|
||||
Container,
|
||||
type Focusable,
|
||||
getEditorKeybindings,
|
||||
getKeybindings,
|
||||
Input,
|
||||
matchesKey,
|
||||
Spacer,
|
||||
Text,
|
||||
truncateToWidth,
|
||||
@@ -18,7 +17,7 @@ import { KeybindingsManager } from "../../../core/keybindings.js";
|
||||
import type { SessionInfo, SessionListProgress } from "../../../core/session-manager.js";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
import { appKey, appKeyHint, keyHint } from "./keybinding-hints.js";
|
||||
import { keyHint, keyText } from "./keybinding-hints.js";
|
||||
import { filterAndSortSessions, hasSessionName, type NameFilter, type SortMode } from "./session-selector-search.js";
|
||||
|
||||
type SessionScope = "current" | "all";
|
||||
@@ -52,7 +51,6 @@ class SessionSelectorHeader implements Component {
|
||||
private scope: SessionScope;
|
||||
private sortMode: SortMode;
|
||||
private nameFilter: NameFilter;
|
||||
private keybindings: KeybindingsManager;
|
||||
private requestRender: () => void;
|
||||
private loading = false;
|
||||
private loadProgress: { loaded: number; total: number } | null = null;
|
||||
@@ -62,17 +60,10 @@ class SessionSelectorHeader implements Component {
|
||||
private statusTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private showRenameHint = false;
|
||||
|
||||
constructor(
|
||||
scope: SessionScope,
|
||||
sortMode: SortMode,
|
||||
nameFilter: NameFilter,
|
||||
keybindings: KeybindingsManager,
|
||||
requestRender: () => void,
|
||||
) {
|
||||
constructor(scope: SessionScope, sortMode: SortMode, nameFilter: NameFilter, requestRender: () => void) {
|
||||
this.scope = scope;
|
||||
this.sortMode = sortMode;
|
||||
this.nameFilter = nameFilter;
|
||||
this.keybindings = keybindings;
|
||||
this.requestRender = requestRender;
|
||||
}
|
||||
|
||||
@@ -159,7 +150,7 @@ class SessionSelectorHeader implements Component {
|
||||
let hintLine1: string;
|
||||
let hintLine2: string;
|
||||
if (this.confirmingDeletePath !== null) {
|
||||
const confirmHint = "Delete session? [Enter] confirm · [Esc/Ctrl+C] cancel";
|
||||
const confirmHint = `Delete session? ${keyHint("tui.select.confirm", "confirm")} · ${keyHint("tui.select.cancel", "cancel")}`;
|
||||
hintLine1 = theme.fg("error", truncateToWidth(confirmHint, width, "…"));
|
||||
hintLine2 = "";
|
||||
} else if (this.statusMessage) {
|
||||
@@ -169,15 +160,16 @@ class SessionSelectorHeader implements Component {
|
||||
} else {
|
||||
const pathState = this.showPath ? "(on)" : "(off)";
|
||||
const sep = theme.fg("muted", " · ");
|
||||
const hint1 = keyHint("tab", "scope") + sep + theme.fg("muted", 're:<pattern> regex · "phrase" exact');
|
||||
const hint1 =
|
||||
keyHint("tui.input.tab", "scope") + sep + theme.fg("muted", 're:<pattern> regex · "phrase" exact');
|
||||
const hint2Parts = [
|
||||
keyHint("toggleSessionSort", "sort"),
|
||||
appKeyHint(this.keybindings, "toggleSessionNamedFilter", "named"),
|
||||
keyHint("deleteSession", "delete"),
|
||||
keyHint("toggleSessionPath", `path ${pathState}`),
|
||||
keyHint("app.session.toggleSort", "sort"),
|
||||
keyHint("app.session.toggleNamedFilter", "named"),
|
||||
keyHint("app.session.delete", "delete"),
|
||||
keyHint("app.session.togglePath", `path ${pathState}`),
|
||||
];
|
||||
if (this.showRenameHint) {
|
||||
hint2Parts.push(keyHint("renameSession", "rename"));
|
||||
hint2Parts.push(keyHint("app.session.rename", "rename"));
|
||||
}
|
||||
const hint2 = hint2Parts.join(sep);
|
||||
hintLine1 = truncateToWidth(hint1, width, "…");
|
||||
@@ -402,7 +394,7 @@ class SessionList implements Component, Focusable {
|
||||
if (this.filteredSessions.length === 0) {
|
||||
let emptyMessage: string;
|
||||
if (this.nameFilter === "named") {
|
||||
const toggleKey = appKey(this.keybindings, "toggleSessionNamedFilter");
|
||||
const toggleKey = keyText("app.session.toggleNamedFilter");
|
||||
if (this.showCwd) {
|
||||
emptyMessage = ` No named sessions found. Press ${toggleKey} to show all.`;
|
||||
} else {
|
||||
@@ -511,18 +503,17 @@ class SessionList implements Component, Focusable {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
|
||||
// Handle delete confirmation state first - intercept all keys
|
||||
if (this.confirmingDeletePath !== null) {
|
||||
if (kb.matches(keyData, "selectConfirm")) {
|
||||
if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const pathToDelete = this.confirmingDeletePath;
|
||||
this.setConfirmingDeletePath(null);
|
||||
void this.onDeleteSession?.(pathToDelete);
|
||||
return;
|
||||
}
|
||||
// Allow both Escape and Ctrl+C to cancel (consistent with pi UX)
|
||||
if (kb.matches(keyData, "selectCancel") || matchesKey(keyData, "ctrl+c")) {
|
||||
if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.setConfirmingDeletePath(null);
|
||||
return;
|
||||
}
|
||||
@@ -530,38 +521,38 @@ class SessionList implements Component, Focusable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (kb.matches(keyData, "tab")) {
|
||||
if (kb.matches(keyData, "tui.input.tab")) {
|
||||
if (this.onToggleScope) {
|
||||
this.onToggleScope();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (kb.matches(keyData, "toggleSessionSort")) {
|
||||
if (kb.matches(keyData, "app.session.toggleSort")) {
|
||||
this.onToggleSort?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.keybindings.matches(keyData, "toggleSessionNamedFilter")) {
|
||||
if (this.keybindings.matches(keyData, "app.session.toggleNamedFilter")) {
|
||||
this.onToggleNameFilter?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+P: toggle path display
|
||||
if (kb.matches(keyData, "toggleSessionPath")) {
|
||||
if (kb.matches(keyData, "app.session.togglePath")) {
|
||||
this.showPath = !this.showPath;
|
||||
this.onTogglePath?.(this.showPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+D: initiate delete confirmation (useful on terminals that don't distinguish Ctrl+Backspace from Backspace)
|
||||
if (kb.matches(keyData, "deleteSession")) {
|
||||
if (kb.matches(keyData, "app.session.delete")) {
|
||||
this.startDeleteConfirmationForSelectedSession();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ctrl+R: rename selected session
|
||||
if (matchesKey(keyData, "ctrl+r")) {
|
||||
// Rename selected session
|
||||
if (kb.matches(keyData, "app.session.rename")) {
|
||||
const selected = this.filteredSessions[this.selectedIndex];
|
||||
if (selected) {
|
||||
this.onRenameSession?.(selected.session.path);
|
||||
@@ -571,7 +562,7 @@ class SessionList implements Component, Focusable {
|
||||
|
||||
// Ctrl+Backspace: non-invasive convenience alias for delete
|
||||
// Only triggers deletion when the query is empty; otherwise it is forwarded to the input
|
||||
if (kb.matches(keyData, "deleteSessionNoninvasive")) {
|
||||
if (kb.matches(keyData, "app.session.deleteNoninvasive")) {
|
||||
if (this.searchInput.getValue().length > 0) {
|
||||
this.searchInput.handleInput(keyData);
|
||||
this.filterSessions(this.searchInput.getValue());
|
||||
@@ -583,30 +574,30 @@ class SessionList implements Component, Focusable {
|
||||
}
|
||||
|
||||
// Up arrow
|
||||
if (kb.matches(keyData, "selectUp")) {
|
||||
if (kb.matches(keyData, "tui.select.up")) {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
||||
}
|
||||
// Down arrow
|
||||
else if (kb.matches(keyData, "selectDown")) {
|
||||
else if (kb.matches(keyData, "tui.select.down")) {
|
||||
this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + 1);
|
||||
}
|
||||
// Page up - jump up by maxVisible items
|
||||
else if (kb.matches(keyData, "selectPageUp")) {
|
||||
else if (kb.matches(keyData, "tui.select.pageUp")) {
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisible);
|
||||
}
|
||||
// Page down - jump down by maxVisible items
|
||||
else if (kb.matches(keyData, "selectPageDown")) {
|
||||
else if (kb.matches(keyData, "tui.select.pageDown")) {
|
||||
this.selectedIndex = Math.min(this.filteredSessions.length - 1, this.selectedIndex + this.maxVisible);
|
||||
}
|
||||
// Enter
|
||||
else if (kb.matches(keyData, "selectConfirm")) {
|
||||
else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const selected = this.filteredSessions[this.selectedIndex];
|
||||
if (selected && this.onSelect) {
|
||||
this.onSelect(selected.session.path);
|
||||
}
|
||||
}
|
||||
// Escape - cancel
|
||||
else if (kb.matches(keyData, "selectCancel")) {
|
||||
else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
@@ -667,8 +658,8 @@ async function deleteSessionFile(
|
||||
export class SessionSelectorComponent extends Container implements Focusable {
|
||||
handleInput(data: string): void {
|
||||
if (this.mode === "rename") {
|
||||
const kb = getEditorKeybindings();
|
||||
if (kb.matches(data, "selectCancel") || matchesKey(data, "ctrl+c")) {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(data, "tui.select.cancel")) {
|
||||
this.exitRenameMode();
|
||||
return;
|
||||
}
|
||||
@@ -749,13 +740,7 @@ export class SessionSelectorComponent extends Container implements Focusable {
|
||||
this.allSessionsLoader = allSessionsLoader;
|
||||
this.onCancel = onCancel;
|
||||
this.requestRender = requestRender;
|
||||
this.header = new SessionSelectorHeader(
|
||||
this.scope,
|
||||
this.sortMode,
|
||||
this.nameFilter,
|
||||
this.keybindings,
|
||||
this.requestRender,
|
||||
);
|
||||
this.header = new SessionSelectorHeader(this.scope, this.sortMode, this.nameFilter, this.requestRender);
|
||||
const renameSession = options?.renameSession;
|
||||
this.renameSession = renameSession;
|
||||
this.canRename = !!renameSession;
|
||||
@@ -864,7 +849,13 @@ export class SessionSelectorComponent extends Container implements Focusable {
|
||||
panel.addChild(new Spacer(1));
|
||||
panel.addChild(this.renameInput);
|
||||
panel.addChild(new Spacer(1));
|
||||
panel.addChild(new Text(theme.fg("muted", "Enter to save · Esc/Ctrl+C to cancel"), 1, 0));
|
||||
panel.addChild(
|
||||
new Text(
|
||||
theme.fg("muted", `${keyText("tui.select.confirm")} to save · ${keyText("tui.select.cancel")} to cancel`),
|
||||
1,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
this.buildBaseLayout(panel, { showHeader: false });
|
||||
this.requestRender();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Markdown, type MarkdownTheme, Text } from "@mariozechner/pi-tui";
|
||||
import type { ParsedSkillBlock } from "../../../core/agent-session.js";
|
||||
import { getMarkdownTheme, theme } from "../theme/theme.js";
|
||||
import { editorKey } from "./keybinding-hints.js";
|
||||
import { keyText } from "./keybinding-hints.js";
|
||||
|
||||
/**
|
||||
* Component that renders a skill invocation message with collapsed/expanded state.
|
||||
@@ -48,7 +48,7 @@ export class SkillInvocationMessageComponent extends Box {
|
||||
const line =
|
||||
theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
|
||||
theme.fg("customMessageText", this.skillBlock.name) +
|
||||
theme.fg("dim", ` (${editorKey("expandTools")} to expand)`);
|
||||
theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
|
||||
this.addChild(new Text(line, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,7 +588,7 @@ export class ToolExecutionComponent extends Container {
|
||||
if (cachedSkipped && cachedSkipped > 0) {
|
||||
const hint =
|
||||
theme.fg("muted", `... (${cachedSkipped} earlier lines,`) +
|
||||
` ${keyHint("expandTools", "to expand")})`;
|
||||
` ${keyHint("app.tools.expand", "to expand")})`;
|
||||
return ["", truncateToWidth(hint, width, "..."), ...cachedLines];
|
||||
}
|
||||
// Add blank line for spacing (matches expanded case)
|
||||
@@ -695,7 +695,7 @@ export class ToolExecutionComponent extends Container {
|
||||
.map((line: string) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line))))
|
||||
.join("\n");
|
||||
if (remaining > 0) {
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
||||
}
|
||||
|
||||
const truncation = this.result.details?.truncation;
|
||||
@@ -772,7 +772,7 @@ export class ToolExecutionComponent extends Container {
|
||||
if (remaining > 0) {
|
||||
text +=
|
||||
theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`) +
|
||||
` ${keyHint("expandTools", "to expand")})`;
|
||||
` ${keyHint("app.tools.expand", "to expand")})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,7 +839,7 @@ export class ToolExecutionComponent extends Container {
|
||||
|
||||
text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
|
||||
if (remaining > 0) {
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -881,7 +881,7 @@ export class ToolExecutionComponent extends Container {
|
||||
|
||||
text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
|
||||
if (remaining > 0) {
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,7 +927,7 @@ export class ToolExecutionComponent extends Container {
|
||||
|
||||
text += `\n\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
|
||||
if (remaining > 0) {
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("expandTools", "to expand")})`;
|
||||
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
type Component,
|
||||
Container,
|
||||
type Focusable,
|
||||
getEditorKeybindings,
|
||||
getKeybindings,
|
||||
Input,
|
||||
matchesKey,
|
||||
Spacer,
|
||||
@@ -860,12 +860,12 @@ class TreeList implements Component {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
if (kb.matches(keyData, "selectUp")) {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "tui.select.up")) {
|
||||
this.selectedIndex = this.selectedIndex === 0 ? this.filteredNodes.length - 1 : this.selectedIndex - 1;
|
||||
} else if (kb.matches(keyData, "selectDown")) {
|
||||
} else if (kb.matches(keyData, "tui.select.down")) {
|
||||
this.selectedIndex = this.selectedIndex === this.filteredNodes.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
} else if (kb.matches(keyData, "treeFoldOrUp")) {
|
||||
} else if (kb.matches(keyData, "app.tree.foldOrUp")) {
|
||||
const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
|
||||
if (currentId && this.isFoldable(currentId) && !this.foldedNodes.has(currentId)) {
|
||||
this.foldedNodes.add(currentId);
|
||||
@@ -873,7 +873,7 @@ class TreeList implements Component {
|
||||
} else {
|
||||
this.selectedIndex = this.findBranchSegmentStart("up");
|
||||
}
|
||||
} else if (kb.matches(keyData, "treeUnfoldOrDown")) {
|
||||
} else if (kb.matches(keyData, "app.tree.unfoldOrDown")) {
|
||||
const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
|
||||
if (currentId && this.foldedNodes.has(currentId)) {
|
||||
this.foldedNodes.delete(currentId);
|
||||
@@ -881,18 +881,18 @@ class TreeList implements Component {
|
||||
} else {
|
||||
this.selectedIndex = this.findBranchSegmentStart("down");
|
||||
}
|
||||
} else if (kb.matches(keyData, "cursorLeft") || kb.matches(keyData, "selectPageUp")) {
|
||||
} else if (kb.matches(keyData, "tui.editor.cursorLeft") || kb.matches(keyData, "tui.select.pageUp")) {
|
||||
// Page up
|
||||
this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisibleLines);
|
||||
} else if (kb.matches(keyData, "cursorRight") || kb.matches(keyData, "selectPageDown")) {
|
||||
} else if (kb.matches(keyData, "tui.editor.cursorRight") || kb.matches(keyData, "tui.select.pageDown")) {
|
||||
// Page down
|
||||
this.selectedIndex = Math.min(this.filteredNodes.length - 1, this.selectedIndex + this.maxVisibleLines);
|
||||
} else if (kb.matches(keyData, "selectConfirm")) {
|
||||
} else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const selected = this.filteredNodes[this.selectedIndex];
|
||||
if (selected && this.onSelect) {
|
||||
this.onSelect(selected.node.entry.id);
|
||||
}
|
||||
} else if (kb.matches(keyData, "selectCancel")) {
|
||||
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
if (this.searchQuery) {
|
||||
this.searchQuery = "";
|
||||
this.foldedNodes.clear();
|
||||
@@ -939,7 +939,7 @@ class TreeList implements Component {
|
||||
this.filterMode = modes[(currentIndex + 1) % modes.length];
|
||||
this.foldedNodes.clear();
|
||||
this.applyFilter();
|
||||
} else if (kb.matches(keyData, "deleteCharBackward")) {
|
||||
} else if (kb.matches(keyData, "tui.editor.deleteCharBackward")) {
|
||||
if (this.searchQuery.length > 0) {
|
||||
this.searchQuery = this.searchQuery.slice(0, -1);
|
||||
this.foldedNodes.clear();
|
||||
@@ -1066,17 +1066,20 @@ class LabelInput implements Component, Focusable {
|
||||
lines.push(truncateToWidth(`${indent}${theme.fg("muted", "Label (empty to remove):")}`, width));
|
||||
lines.push(...this.input.render(availableWidth).map((line) => truncateToWidth(`${indent}${line}`, width)));
|
||||
lines.push(
|
||||
truncateToWidth(`${indent}${keyHint("selectConfirm", "save")} ${keyHint("selectCancel", "cancel")}`, width),
|
||||
truncateToWidth(
|
||||
`${indent}${keyHint("tui.select.confirm", "save")} ${keyHint("tui.select.cancel", "cancel")}`,
|
||||
width,
|
||||
),
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
if (kb.matches(keyData, "selectConfirm")) {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const value = this.input.getValue().trim();
|
||||
this.onSubmit?.(this.entryId, value || undefined);
|
||||
} else if (kb.matches(keyData, "selectCancel")) {
|
||||
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
this.onCancel?.();
|
||||
} else {
|
||||
this.input.handleInput(keyData);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type Component, Container, getEditorKeybindings, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { type Component, Container, getKeybindings, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
@@ -78,24 +78,24 @@ class UserMessageList implements Component {
|
||||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
const kb = getEditorKeybindings();
|
||||
const kb = getKeybindings();
|
||||
// Up arrow - go to previous (older) message, wrap to bottom when at top
|
||||
if (kb.matches(keyData, "selectUp")) {
|
||||
if (kb.matches(keyData, "tui.select.up")) {
|
||||
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 (kb.matches(keyData, "selectDown")) {
|
||||
else if (kb.matches(keyData, "tui.select.down")) {
|
||||
this.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;
|
||||
}
|
||||
// Enter - select message and branch
|
||||
else if (kb.matches(keyData, "selectConfirm")) {
|
||||
else if (kb.matches(keyData, "tui.select.confirm")) {
|
||||
const selected = this.messages[this.selectedIndex];
|
||||
if (selected && this.onSelect) {
|
||||
this.onSelect(selected.id);
|
||||
}
|
||||
}
|
||||
// Escape - cancel
|
||||
else if (kb.matches(keyData, "selectCancel")) {
|
||||
else if (kb.matches(keyData, "tui.select.cancel")) {
|
||||
if (this.onCancel) {
|
||||
this.onCancel();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user