Merge pull request #315 from mitsuhiko/model-switcher

Reverse model switching and binding for dialog
This commit is contained in:
Mario Zechner 2025-12-25 18:33:42 +01:00 committed by GitHub
commit 4edfff41a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 12 deletions

View file

@ -580,18 +580,19 @@ export class AgentSession {
} }
/** /**
* Cycle to next model. * Cycle to next/previous model.
* Uses scoped models (from --models flag) if available, otherwise all available models. * Uses scoped models (from --models flag) if available, otherwise all available models.
* @param direction - "forward" (default) or "backward"
* @returns The new model info, or null if only one model available * @returns The new model info, or null if only one model available
*/ */
async cycleModel(): Promise<ModelCycleResult | null> { async cycleModel(direction: "forward" | "backward" = "forward"): Promise<ModelCycleResult | null> {
if (this._scopedModels.length > 0) { if (this._scopedModels.length > 0) {
return this._cycleScopedModel(); return this._cycleScopedModel(direction);
} }
return this._cycleAvailableModel(); return this._cycleAvailableModel(direction);
} }
private async _cycleScopedModel(): Promise<ModelCycleResult | null> { private async _cycleScopedModel(direction: "forward" | "backward"): Promise<ModelCycleResult | null> {
if (this._scopedModels.length <= 1) return null; if (this._scopedModels.length <= 1) return null;
const currentModel = this.model; const currentModel = this.model;
@ -600,7 +601,8 @@ export class AgentSession {
); );
if (currentIndex === -1) currentIndex = 0; if (currentIndex === -1) currentIndex = 0;
const nextIndex = (currentIndex + 1) % this._scopedModels.length; const len = this._scopedModels.length;
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
const next = this._scopedModels[nextIndex]; const next = this._scopedModels[nextIndex];
// Validate API key // Validate API key
@ -620,7 +622,7 @@ export class AgentSession {
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true }; return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
} }
private async _cycleAvailableModel(): Promise<ModelCycleResult | null> { private async _cycleAvailableModel(direction: "forward" | "backward"): Promise<ModelCycleResult | null> {
const availableModels = await this._modelRegistry.getAvailable(); const availableModels = await this._modelRegistry.getAvailable();
if (availableModels.length <= 1) return null; if (availableModels.length <= 1) return null;
@ -630,7 +632,8 @@ export class AgentSession {
); );
if (currentIndex === -1) currentIndex = 0; if (currentIndex === -1) currentIndex = 0;
const nextIndex = (currentIndex + 1) % availableModels.length; const len = availableModels.length;
const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
const nextModel = availableModels[nextIndex]; const nextModel = availableModels[nextIndex];
const apiKey = await this._modelRegistry.getApiKey(nextModel); const apiKey = await this._modelRegistry.getApiKey(nextModel);

View file

@ -3,11 +3,13 @@ import {
isCtrlC, isCtrlC,
isCtrlD, isCtrlD,
isCtrlG, isCtrlG,
isCtrlL,
isCtrlO, isCtrlO,
isCtrlP, isCtrlP,
isCtrlT, isCtrlT,
isCtrlZ, isCtrlZ,
isEscape, isEscape,
isShiftCtrlP,
isShiftTab, isShiftTab,
} from "@mariozechner/pi-tui"; } from "@mariozechner/pi-tui";
@ -20,6 +22,8 @@ export class CustomEditor extends Editor {
public onCtrlD?: () => void; public onCtrlD?: () => void;
public onShiftTab?: () => void; public onShiftTab?: () => void;
public onCtrlP?: () => void; public onCtrlP?: () => void;
public onShiftCtrlP?: () => void;
public onCtrlL?: () => void;
public onCtrlO?: () => void; public onCtrlO?: () => void;
public onCtrlT?: () => void; public onCtrlT?: () => void;
public onCtrlG?: () => void; public onCtrlG?: () => void;
@ -44,12 +48,24 @@ export class CustomEditor extends Editor {
return; return;
} }
// Intercept Ctrl+L for model selector
if (isCtrlL(data) && this.onCtrlL) {
this.onCtrlL();
return;
}
// Intercept Ctrl+O for tool output expansion // Intercept Ctrl+O for tool output expansion
if (isCtrlO(data) && this.onCtrlO) { if (isCtrlO(data) && this.onCtrlO) {
this.onCtrlO(); this.onCtrlO();
return; return;
} }
// Intercept Shift+Ctrl+P for backward model cycling (check before Ctrl+P)
if (isShiftCtrlP(data) && this.onShiftCtrlP) {
this.onShiftCtrlP();
return;
}
// Intercept Ctrl+P for model cycling // Intercept Ctrl+P for model cycling
if (isCtrlP(data) && this.onCtrlP) { if (isCtrlP(data) && this.onCtrlP) {
this.onCtrlP(); this.onCtrlP();

View file

@ -213,9 +213,12 @@ export class InteractiveMode {
theme.fg("dim", "shift+tab") + theme.fg("dim", "shift+tab") +
theme.fg("muted", " to cycle thinking") + theme.fg("muted", " to cycle thinking") +
"\n" + "\n" +
theme.fg("dim", "ctrl+p") + theme.fg("dim", "ctrl+p/shift+ctrl+p") +
theme.fg("muted", " to cycle models") + theme.fg("muted", " to cycle models") +
"\n" + "\n" +
theme.fg("dim", "ctrl+l") +
theme.fg("muted", " to select model") +
"\n" +
theme.fg("dim", "ctrl+o") + theme.fg("dim", "ctrl+o") +
theme.fg("muted", " to expand tools") + theme.fg("muted", " to expand tools") +
"\n" + "\n" +
@ -580,7 +583,9 @@ export class InteractiveMode {
this.editor.onCtrlD = () => this.handleCtrlD(); this.editor.onCtrlD = () => this.handleCtrlD();
this.editor.onCtrlZ = () => this.handleCtrlZ(); this.editor.onCtrlZ = () => this.handleCtrlZ();
this.editor.onShiftTab = () => this.cycleThinkingLevel(); this.editor.onShiftTab = () => this.cycleThinkingLevel();
this.editor.onCtrlP = () => this.cycleModel(); this.editor.onCtrlP = () => this.cycleModel("forward");
this.editor.onShiftCtrlP = () => this.cycleModel("backward");
this.editor.onCtrlL = () => this.showModelSelector();
this.editor.onCtrlO = () => this.toggleToolOutputExpansion(); this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility(); this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
this.editor.onCtrlG = () => this.openExternalEditor(); this.editor.onCtrlG = () => this.openExternalEditor();
@ -1200,9 +1205,9 @@ export class InteractiveMode {
} }
} }
private async cycleModel(): Promise<void> { private async cycleModel(direction: "forward" | "backward"): Promise<void> {
try { try {
const result = await this.session.cycleModel(); const result = await this.session.cycleModel(direction);
if (result === null) { if (result === null) {
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available"; const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
this.showStatus(msg); this.showStatus(msg);

View file

@ -36,6 +36,7 @@ export {
isCtrlE, isCtrlE,
isCtrlG, isCtrlG,
isCtrlK, isCtrlK,
isCtrlL,
isCtrlLeft, isCtrlLeft,
isCtrlO, isCtrlO,
isCtrlP, isCtrlP,
@ -49,6 +50,7 @@ export {
isEnter, isEnter,
isEscape, isEscape,
isHome, isHome,
isShiftCtrlP,
isShiftEnter, isShiftEnter,
isShiftTab, isShiftTab,
isTab, isTab,

View file

@ -30,6 +30,7 @@ const CODEPOINTS = {
e: 101, e: 101,
g: 103, g: 103,
k: 107, k: 107,
l: 108,
o: 111, o: 111,
p: 112, p: 112,
t: 116, t: 116,
@ -164,6 +165,7 @@ export const Keys = {
CTRL_E: kittySequence(CODEPOINTS.e, MODIFIERS.ctrl), CTRL_E: kittySequence(CODEPOINTS.e, MODIFIERS.ctrl),
CTRL_G: kittySequence(CODEPOINTS.g, MODIFIERS.ctrl), CTRL_G: kittySequence(CODEPOINTS.g, MODIFIERS.ctrl),
CTRL_K: kittySequence(CODEPOINTS.k, MODIFIERS.ctrl), CTRL_K: kittySequence(CODEPOINTS.k, MODIFIERS.ctrl),
CTRL_L: kittySequence(CODEPOINTS.l, MODIFIERS.ctrl),
CTRL_O: kittySequence(CODEPOINTS.o, MODIFIERS.ctrl), CTRL_O: kittySequence(CODEPOINTS.o, MODIFIERS.ctrl),
CTRL_P: kittySequence(CODEPOINTS.p, MODIFIERS.ctrl), CTRL_P: kittySequence(CODEPOINTS.p, MODIFIERS.ctrl),
CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl), CTRL_T: kittySequence(CODEPOINTS.t, MODIFIERS.ctrl),
@ -220,6 +222,7 @@ const RAW = {
CTRL_E: "\x05", CTRL_E: "\x05",
CTRL_G: "\x07", CTRL_G: "\x07",
CTRL_K: "\x0b", CTRL_K: "\x0b",
CTRL_L: "\x0c",
CTRL_O: "\x0f", CTRL_O: "\x0f",
CTRL_P: "\x10", CTRL_P: "\x10",
CTRL_T: "\x14", CTRL_T: "\x14",
@ -285,6 +288,14 @@ export function isCtrlK(data: string): boolean {
); );
} }
/**
* Check if input matches Ctrl+L (raw byte or Kitty protocol).
* Ignores lock key bits.
*/
export function isCtrlL(data: string): boolean {
return data === RAW.CTRL_L || data === Keys.CTRL_L || matchesKittySequence(data, CODEPOINTS.l, MODIFIERS.ctrl);
}
/** /**
* Check if input matches Ctrl+O (raw byte or Kitty protocol). * Check if input matches Ctrl+O (raw byte or Kitty protocol).
* Ignores lock key bits. * Ignores lock key bits.
@ -301,6 +312,14 @@ export function isCtrlP(data: string): boolean {
return data === RAW.CTRL_P || data === Keys.CTRL_P || matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.ctrl); return data === RAW.CTRL_P || data === Keys.CTRL_P || matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.ctrl);
} }
/**
* Check if input matches Shift+Ctrl+P (Kitty protocol only).
* Ignores lock key bits.
*/
export function isShiftCtrlP(data: string): boolean {
return matchesKittySequence(data, CODEPOINTS.p, MODIFIERS.shift + MODIFIERS.ctrl);
}
/** /**
* Check if input matches Ctrl+T (raw byte or Kitty protocol). * Check if input matches Ctrl+T (raw byte or Kitty protocol).
* Ignores lock key bits. * Ignores lock key bits.