feat: enhance model cycling with thinking levels and --thinking flag

PR #47 enhancements:
- Add thinking level syntax to --models (e.g., --models sonnet:high,haiku:low)
- First model in scope used as initial model when starting new session
- Auto-apply thinking level when cycling models with Ctrl+P
- Save both model and thinking to session AND settings for persistence
- Simplify UX by removing autoThinkingDisabled flag
- Fix model matching to prioritize exact matches over partial
- Support provider/modelId format (e.g., openrouter/openai/gpt-5.1-codex)

Issue #45:
- Add --thinking CLI flag to set thinking level directly
- Takes highest priority over all other thinking level sources

Closes #45
This commit is contained in:
Mario Zechner 2025-11-21 21:32:30 +01:00
parent df3af27288
commit ba8c073ed2
5 changed files with 117 additions and 31 deletions

View file

@ -39,6 +39,7 @@ interface Args {
model?: string;
apiKey?: string;
systemPrompt?: string;
thinking?: ThinkingLevel;
continue?: boolean;
resume?: boolean;
help?: boolean;
@ -82,6 +83,17 @@ function parseArgs(args: string[]): Args {
result.session = args[++i];
} else if (arg === "--models" && i + 1 < args.length) {
result.models = args[++i].split(",").map((s) => s.trim());
} else if (arg === "--thinking" && i + 1 < args.length) {
const level = args[++i];
if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high") {
result.thinking = level;
} else {
console.error(
chalk.yellow(
`Warning: Invalid thinking level "${level}". Valid values: off, minimal, low, medium, high`,
),
);
}
} else if (!arg.startsWith("-")) {
result.messages.push(arg);
}
@ -107,6 +119,7 @@ ${chalk.bold("Options:")}
--session <path> Use specific session file
--no-session Don't save session (ephemeral)
--models <patterns> Comma-separated model patterns for quick cycling with Ctrl+P
--thinking <level> Set thinking level: off, minimal, low, medium, high
--help, -h Show this help
${chalk.bold("Examples:")}
@ -131,6 +144,9 @@ ${chalk.bold("Examples:")}
# Cycle models with fixed thinking levels
pi --models sonnet:high,haiku:low
# Start with a specific thinking level
pi --thinking high "Solve this complex problem"
${chalk.bold("Environment Variables:")}
ANTHROPIC_API_KEY - Anthropic Claude API key
ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth token (alternative to API key)
@ -370,7 +386,38 @@ async function resolveModelScope(
}
}
// Find all models matching this pattern (case-insensitive partial match)
// Check for provider/modelId format (provider is everything before the first /)
const slashIndex = modelPattern.indexOf("/");
if (slashIndex !== -1) {
const provider = modelPattern.substring(0, slashIndex);
const modelId = modelPattern.substring(slashIndex + 1);
const providerMatch = availableModels.find(
(m) => m.provider.toLowerCase() === provider.toLowerCase() && m.id.toLowerCase() === modelId.toLowerCase(),
);
if (providerMatch) {
if (
!scopedModels.find(
(sm) => sm.model.id === providerMatch.id && sm.model.provider === providerMatch.provider,
)
) {
scopedModels.push({ model: providerMatch, thinkingLevel });
}
continue;
}
// No exact provider/model match - fall through to other matching
}
// Check for exact ID match (case-insensitive)
const exactMatch = availableModels.find((m) => m.id.toLowerCase() === modelPattern.toLowerCase());
if (exactMatch) {
// Exact match found - use it directly
if (!scopedModels.find((sm) => sm.model.id === exactMatch.id && sm.model.provider === exactMatch.provider)) {
scopedModels.push({ model: exactMatch, thinkingLevel });
}
continue;
}
// No exact match - fall back to partial matching
const matches = availableModels.filter(
(m) =>
m.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
@ -637,6 +684,12 @@ export async function main(args: string[]) {
process.exit(1);
}
initialModel = model;
// Also load saved thinking level if we're using saved model
const savedThinking = settingsManager.getDefaultThinkingLevel();
if (savedThinking) {
initialThinking = savedThinking;
}
}
}
@ -769,6 +822,11 @@ export async function main(args: string[]) {
}
}
// CLI --thinking flag takes highest priority
if (parsed.thinking) {
initialThinking = parsed.thinking;
}
// Create agent (initialModel can be null in interactive mode)
const agent = new Agent({
initialState: {

View file

@ -6,6 +6,7 @@ export interface Settings {
lastChangelogVersion?: string;
defaultProvider?: string;
defaultModel?: string;
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high";
queueMode?: "all" | "one-at-a-time";
theme?: string;
}
@ -98,4 +99,13 @@ export class SettingsManager {
this.settings.theme = theme;
this.save();
}
getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | undefined {
return this.settings.defaultThinkingLevel;
}
setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high"): void {
this.settings.defaultThinkingLevel = level;
this.save();
}
}

View file

@ -91,9 +91,6 @@ export class TuiRenderer {
// Model scope for quick cycling
private scopedModels: Array<{ model: Model<any>; thinkingLevel: ThinkingLevel }> = [];
// Track if user manually changed thinking (disables auto-thinking from model cycling)
private autoThinkingDisabled = false;
// Tool output expansion state
private toolOutputExpanded = false;
@ -789,11 +786,9 @@ export class TuiRenderer {
// Apply the new thinking level
this.agent.setThinkingLevel(nextLevel);
// Disable auto-thinking since user manually changed it
this.autoThinkingDisabled = true;
// Save thinking level change to session
// Save thinking level change to session and settings
this.sessionManager.saveThinkingLevelChange(nextLevel);
this.settingsManager.setDefaultThinkingLevel(nextLevel);
// Update border color
this.updateEditorBorderColor();
@ -840,17 +835,16 @@ export class TuiRenderer {
// Switch model
this.agent.setModel(nextModel);
// Apply thinking level if not disabled and model supports it
if (!this.autoThinkingDisabled && nextModel.reasoning) {
this.agent.setThinkingLevel(nextThinking);
this.sessionManager.saveThinkingLevelChange(nextThinking);
this.updateEditorBorderColor();
} else if (!this.autoThinkingDisabled && !nextModel.reasoning && nextThinking !== "off") {
// Model doesn't support thinking but user requested it - silently ignore
this.agent.setThinkingLevel("off");
this.sessionManager.saveThinkingLevelChange("off");
this.updateEditorBorderColor();
}
// Save model change to session and settings
this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
// Apply thinking level (silently use "off" if model doesn't support thinking)
const effectiveThinking = nextModel.reasoning ? nextThinking : "off";
this.agent.setThinkingLevel(effectiveThinking);
this.sessionManager.saveThinkingLevelChange(effectiveThinking);
this.settingsManager.setDefaultThinkingLevel(effectiveThinking);
this.updateEditorBorderColor();
// Show notification
this.chatContainer.addChild(new Spacer(1));
@ -874,7 +868,7 @@ export class TuiRenderer {
if (availableModels.length === 1) {
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model available"), 1, 0));
this.ui.requestRender();
return;
}
@ -902,6 +896,10 @@ export class TuiRenderer {
// Switch model
this.agent.setModel(nextModel);
// Save model change to session and settings
this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
// Show notification
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
@ -949,11 +947,9 @@ export class TuiRenderer {
// Apply the selected thinking level
this.agent.setThinkingLevel(level);
// Disable auto-thinking since user manually changed it
this.autoThinkingDisabled = true;
// Save thinking level change to session
// Save thinking level change to session and settings
this.sessionManager.saveThinkingLevelChange(level);
this.settingsManager.setDefaultThinkingLevel(level);
// Update border color
this.updateEditorBorderColor();
@ -1107,9 +1103,6 @@ export class TuiRenderer {
// Apply the selected model
this.agent.setModel(model);
// Clear scoped models since user manually selected a model
this.scopedModels = [];
// Save model change to session
this.sessionManager.saveModelChange(model.provider, model.id);