mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
parent
47236c8445
commit
92d8e2d17d
8 changed files with 84 additions and 19 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed configurable save keybindings in the model and thinking selectors ([#8797](https://github.com/earendil-works/pi/issues/8797)).
|
||||
|
||||
## [0.85.0] - 2026-09-04
|
||||
|
||||
### New Features
|
||||
|
|
|
|||
|
|
@ -151,7 +151,9 @@ This routing remains configurable through the ordinary action bindings. For exam
|
|||
| `app.model.select` | `ctrl+l` | Open model selector |
|
||||
| `app.model.cycleForward` | `ctrl+p` | Cycle to next model |
|
||||
| `app.model.cycleBackward` | `shift+ctrl+p` (`alt+p` on Windows and WSL) | Cycle to previous model |
|
||||
| `app.models.save` | `ctrl+s` | Save the selected default model or scoped model configuration to settings |
|
||||
| `app.thinking.cycle` | `shift+tab` | Cycle thinking level |
|
||||
| `app.thinking.save` | `ctrl+s` | Save current thinking level to settings |
|
||||
| `app.thinking.toggle` | `ctrl+t` | Collapse or expand thinking blocks |
|
||||
|
||||
### Display and Message Queue
|
||||
|
|
@ -185,7 +187,6 @@ Used inside the scoped models selector (opened via `/scoped-models`).
|
|||
|
||||
| Keybinding id | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `app.models.save` | `ctrl+s` | Save current model selection to settings |
|
||||
| `app.models.enableAll` | `ctrl+a` | Enable all models (or all matching the current search) |
|
||||
| `app.models.clearAll` | `ctrl+x` | Clear all models (or all matching the current search) |
|
||||
| `app.models.toggleProvider` | `ctrl+p` | Toggle all models for the current provider |
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface AppKeybindings {
|
|||
"app.exit": true;
|
||||
"app.suspend": true;
|
||||
"app.thinking.cycle": true;
|
||||
"app.thinking.save": true;
|
||||
"app.model.cycleForward": true;
|
||||
"app.model.cycleBackward": true;
|
||||
"app.model.select": true;
|
||||
|
|
@ -100,6 +101,10 @@ export const KEYBINDINGS = {
|
|||
defaultKeys: "shift+tab",
|
||||
description: "Cycle thinking level",
|
||||
},
|
||||
"app.thinking.save": {
|
||||
defaultKeys: "ctrl+s",
|
||||
description: "Save thinking level",
|
||||
},
|
||||
"app.model.cycleForward": {
|
||||
defaultKeys: "ctrl+p",
|
||||
description: "Cycle to next model",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
fuzzyFilter,
|
||||
getKeybindings,
|
||||
Input,
|
||||
matchesKey,
|
||||
Spacer,
|
||||
Text,
|
||||
type TUI,
|
||||
|
|
@ -15,7 +14,7 @@ import { refreshModelCatalogs } from "../model-catalog-refresh.ts";
|
|||
import { getModelSelectorSearchText } from "../model-search.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyHint } from "./keybinding-hints.ts";
|
||||
import { keyDisplayText, keyHint } from "./keybinding-hints.ts";
|
||||
|
||||
interface ModelItem {
|
||||
provider: string;
|
||||
|
|
@ -137,7 +136,14 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
// Hint
|
||||
if (this.onSelectAsDefaultCallback) {
|
||||
this.addChild(
|
||||
new Text(theme.fg("dim", " Enter to select \u00b7 Ctrl+S to set as default \u00b7 Esc to cancel"), 0, 0),
|
||||
new Text(
|
||||
theme.fg(
|
||||
"dim",
|
||||
` ${keyDisplayText("tui.select.confirm")} to select · ${keyDisplayText("app.models.save")} to set as default · ${keyDisplayText("tui.select.cancel")} to cancel`,
|
||||
),
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -389,8 +395,8 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|||
this.dispose();
|
||||
this.onCancelCallback();
|
||||
}
|
||||
// Ctrl+S — select and save as default
|
||||
else if (matchesKey(keyData, "ctrl+s") && this.onSelectAsDefaultCallback) {
|
||||
// Select and save as default
|
||||
else if (kb.matches(keyData, "app.models.save") && this.onSelectAsDefaultCallback) {
|
||||
const selectedModel = this.filteredModels[this.selectedIndex];
|
||||
if (selectedModel) {
|
||||
this.dispose();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
import { getModelSearchText } from "../model-search.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { DynamicBorder } from "./dynamic-border.ts";
|
||||
import { keyText } from "./keybinding-hints.ts";
|
||||
import { keyDisplayText } from "./keybinding-hints.ts";
|
||||
|
||||
// EnabledIds: null = all enabled (no filter), string[] = explicit ordered list
|
||||
type EnabledIds = string[] | null;
|
||||
|
|
@ -136,7 +136,7 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
|
|||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0));
|
||||
this.addChild(
|
||||
new Text(theme.fg("muted", `Session-only. ${keyText("app.models.save")} to save to settings.`), 0, 0),
|
||||
new Text(theme.fg("muted", `Session-only. ${keyDisplayText("app.models.save")} to save to settings.`), 0, 0),
|
||||
);
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
|
|
@ -200,12 +200,12 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
|
|||
? "all enabled"
|
||||
: `${enabledCount}/${this.allIds.length} enabled${unavailableCount ? ` · ${unavailableCount} unavailable` : ""}`;
|
||||
const parts = [
|
||||
`${keyText("tui.select.confirm")} toggle`,
|
||||
`${keyText("app.models.enableAll")} all`,
|
||||
`${keyText("app.models.clearAll")} clear`,
|
||||
`${keyText("app.models.toggleProvider")} provider`,
|
||||
`${keyText("app.models.reorderUp")}/${keyText("app.models.reorderDown")} reorder`,
|
||||
`${keyText("app.models.save")} save`,
|
||||
`${keyDisplayText("tui.select.confirm")} toggle`,
|
||||
`${keyDisplayText("app.models.enableAll")} all`,
|
||||
`${keyDisplayText("app.models.clearAll")} clear`,
|
||||
`${keyDisplayText("app.models.toggleProvider")} provider`,
|
||||
`${keyDisplayText("app.models.reorderUp")}/${keyDisplayText("app.models.reorderDown")} reorder`,
|
||||
`${keyDisplayText("app.models.save")} save`,
|
||||
countText,
|
||||
];
|
||||
return this.isDirty
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
fuzzyFilter,
|
||||
getKeybindings,
|
||||
Input,
|
||||
matchesKey,
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
type SelectListLayoutOptions,
|
||||
|
|
@ -91,7 +90,16 @@ export class ThinkingSelectorComponent extends Container implements Focusable {
|
|||
this.selectListChildIndex = this.children.length;
|
||||
this.addChild(this.selectList);
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.fg("dim", " Enter to select · Ctrl+S to set as default · Esc to cancel"), 0, 0));
|
||||
this.addChild(
|
||||
new Text(
|
||||
theme.fg(
|
||||
"dim",
|
||||
` ${keyDisplayText("tui.select.confirm")} to select · ${keyDisplayText("app.thinking.save")} to set as default · ${keyDisplayText("tui.select.cancel")} to cancel`,
|
||||
),
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
// Add bottom border
|
||||
this.addChild(new DynamicBorder());
|
||||
|
|
@ -119,13 +127,13 @@ export class ThinkingSelectorComponent extends Container implements Focusable {
|
|||
}
|
||||
|
||||
handleInput(keyData: string): void {
|
||||
if (matchesKey(keyData, "ctrl+s") && this.onSelectAsDefault) {
|
||||
const kb = getKeybindings();
|
||||
if (kb.matches(keyData, "app.thinking.save") && this.onSelectAsDefault) {
|
||||
const item = this.selectList.getSelectedItem();
|
||||
if (item) this.onSelectAsDefault(item.value as ThinkingLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
const kb = getKeybindings();
|
||||
const isNav =
|
||||
kb.matches(keyData, "tui.select.up") ||
|
||||
kb.matches(keyData, "tui.select.down") ||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,29 @@ describe("model selector", () => {
|
|||
selector.dispose();
|
||||
});
|
||||
|
||||
it("uses the configured save binding", async () => {
|
||||
setKeybindings(new KeybindingsManager({ "app.models.save": "ctrl+r" }));
|
||||
harness = await createHarness();
|
||||
const currentModel = harness.getModel()!;
|
||||
const saveDefault = vi.fn();
|
||||
const selector = new ModelSelectorComponent(
|
||||
createFakeTui(),
|
||||
currentModel,
|
||||
harness.session.modelRuntime,
|
||||
[],
|
||||
() => {},
|
||||
() => {},
|
||||
undefined,
|
||||
saveDefault,
|
||||
);
|
||||
|
||||
expect(stripAnsi(selector.render(120).join("\n"))).toContain("Ctrl+R to set as default");
|
||||
selector.handleInput("\x13");
|
||||
expect(saveDefault).not.toHaveBeenCalled();
|
||||
selector.handleInput("\x12");
|
||||
expect(saveDefault).toHaveBeenCalledWith(currentModel);
|
||||
});
|
||||
|
||||
it("lists every catalog that failed to refresh", async () => {
|
||||
harness = await createHarness();
|
||||
vi.spyOn(harness.session.modelRuntime, "refresh").mockResolvedValue({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { setKeybindings } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.ts";
|
||||
import { ThinkingSelectorComponent } from "../src/modes/interactive/components/thinking-selector.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
|
|
@ -34,4 +34,22 @@ describe("thinking selector", () => {
|
|||
expect(getLevelRow("medium")?.startsWith(" ✓ medium")).toBe(true);
|
||||
expect(getLevelRow("high")?.startsWith("→ high")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the configured save binding", () => {
|
||||
setKeybindings(new KeybindingsManager({ "app.thinking.save": "ctrl+r" }));
|
||||
const saveDefault = vi.fn();
|
||||
const selector = new ThinkingSelectorComponent(
|
||||
"medium",
|
||||
["medium", "high"],
|
||||
() => {},
|
||||
() => {},
|
||||
saveDefault,
|
||||
);
|
||||
|
||||
expect(stripAnsi(selector.render(80).join("\n"))).toContain("Ctrl+R to set as default");
|
||||
selector.handleInput("\x13");
|
||||
expect(saveDefault).not.toHaveBeenCalled();
|
||||
selector.handleInput("\x12");
|
||||
expect(saveDefault).toHaveBeenCalledWith("medium");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue