feat(coding-agent): added temp model controls and auto-chdir
This commit is contained in:
@@ -4,7 +4,15 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Added temporary model selection: `Ctrl+Y` opens model selector for session-only model switching (not persisted to settings)
|
||||
- Added `setModelTemporary()` method to AgentSession for ephemeral model changes
|
||||
- Added empty Enter to flush queued messages: pressing Enter with empty editor while streaming aborts current stream
|
||||
- Added auto-chdir to temp directories when starting in home unless `--allow-home` is set
|
||||
- Added upfront diff parsing and filtering for code review command to exclude lock files, generated code, and binary assets
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed auto-chdir to only use existing directories and fall back to `tmpdir()`
|
||||
- Added automatic reviewer agent count recommendation based on diff weight and file count
|
||||
- Added file grouping guidance for parallel review distribution across multiple agents
|
||||
- Added diff preview mode for large changesets that exceed size thresholds
|
||||
@@ -29,6 +37,9 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `Ctrl+P` to cycle through role models (slow → default → smol) instead of all available models
|
||||
- Changed `Shift+Ctrl+P` to cycle role models temporarily (not persisted)
|
||||
- Changed Extension Control Center to scale with terminal height instead of fixed 25-line limit
|
||||
- Changed review command to parse git diff upfront and provide structured context to reviewer agents
|
||||
- Changed session persistence to use structured logging instead of console.error for persistence failures
|
||||
- Changed find tool to use fd command for .gitignore discovery instead of Bun.Glob for better abort handling
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Mode = "text" | "json" | "rpc";
|
||||
|
||||
export interface Args {
|
||||
cwd?: string;
|
||||
allowHome?: boolean;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
smol?: string;
|
||||
@@ -62,6 +63,8 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
||||
result.help = true;
|
||||
} else if (arg === "--version" || arg === "-v") {
|
||||
result.version = true;
|
||||
} else if (arg === "--allow-home") {
|
||||
result.allowHome = true;
|
||||
} else if (arg === "--mode" && i + 1 < args.length) {
|
||||
const mode = args[++i];
|
||||
if (mode === "text" || mode === "json" || mode === "rpc") {
|
||||
@@ -176,6 +179,7 @@ ${chalk.bold("Options:")}
|
||||
--api-key <key> API key (defaults to env vars)
|
||||
--system-prompt <text> System prompt (default: coding assistant prompt)
|
||||
--append-system-prompt <text> Append text or file contents to the system prompt
|
||||
--allow-home Allow starting in ~ without auto-switching to a temp dir
|
||||
--mode <mode> Output mode: text (default), json, or rpc
|
||||
--print, -p Non-interactive mode: process prompt and exit
|
||||
--continue, -c Continue previous session
|
||||
|
||||
@@ -1128,6 +1128,24 @@ export class AgentSession {
|
||||
this.setThinkingLevel(this.thinkingLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set model temporarily (for this session only).
|
||||
* Validates API key, saves to session log but NOT to settings.
|
||||
* @throws Error if no API key available for the model
|
||||
*/
|
||||
async setModelTemporary(model: Model<any>): Promise<void> {
|
||||
const apiKey = await this._modelRegistry.getApiKey(model);
|
||||
if (!apiKey) {
|
||||
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
||||
}
|
||||
|
||||
this.agent.setModel(model);
|
||||
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
||||
|
||||
// Re-clamp thinking level for new model's capabilities
|
||||
this.setThinkingLevel(this.thinkingLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycle to next/previous model.
|
||||
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
||||
@@ -1144,8 +1162,13 @@ export class AgentSession {
|
||||
/**
|
||||
* Cycle through configured role models in a fixed order.
|
||||
* Skips missing roles and deduplicates models.
|
||||
* @param roleOrder - Order of roles to cycle through (e.g., ["slow", "default", "smol"])
|
||||
* @param options - Optional settings: `temporary` to not persist to settings
|
||||
*/
|
||||
async cycleRoleModels(roleOrder: string[]): Promise<RoleModelCycleResult | undefined> {
|
||||
async cycleRoleModels(
|
||||
roleOrder: string[],
|
||||
options?: { temporary?: boolean },
|
||||
): Promise<RoleModelCycleResult | undefined> {
|
||||
const availableModels = this._modelRegistry.getAvailable();
|
||||
if (availableModels.length === 0) return undefined;
|
||||
|
||||
@@ -1185,7 +1208,11 @@ export class AgentSession {
|
||||
const nextIndex = (currentIndex + 1) % roleModels.length;
|
||||
const next = roleModels[nextIndex];
|
||||
|
||||
await this.setModel(next.model, next.role);
|
||||
if (options?.temporary) {
|
||||
await this.setModelTemporary(next.model);
|
||||
} else {
|
||||
await this.setModel(next.model, next.role);
|
||||
}
|
||||
|
||||
return { model: next.model, thinkingLevel: this.thinkingLevel, role: next.role };
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export {
|
||||
type LspServerStatus,
|
||||
type LspToolDetails,
|
||||
type LspWarmupResult,
|
||||
lspTool,
|
||||
warmupLspServers,
|
||||
} from "./lsp/index";
|
||||
export { createNotebookTool, type NotebookToolDetails } from "./notebook";
|
||||
@@ -149,10 +148,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
||||
|
||||
const entries = requestedTools
|
||||
? requestedTools.filter((name) => name in allTools).map((name) => [name, allTools[name]] as const)
|
||||
: [
|
||||
...Object.entries(BUILTIN_TOOLS),
|
||||
...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : []),
|
||||
];
|
||||
: [...Object.entries(BUILTIN_TOOLS), ...(includeComplete ? ([["complete", HIDDEN_TOOLS.complete]] as const) : [])];
|
||||
const results = await Promise.all(entries.map(([, factory]) => factory(session)));
|
||||
const tools = results.filter((t): t is Tool => t !== null);
|
||||
|
||||
|
||||
@@ -1393,11 +1393,3 @@ export function createLspTool(session: ToolSession): AgentTool<typeof lspSchema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const lspTool = createLspTool({
|
||||
cwd: process.cwd(),
|
||||
hasUI: false,
|
||||
rulebookRules: [],
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => null,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
|
||||
import chalk from "chalk";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { type Args, parseArgs, printHelp } from "./cli/args";
|
||||
import { processFileArguments } from "./cli/file-processor";
|
||||
import { listModels } from "./cli/list-models";
|
||||
@@ -187,6 +189,59 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
||||
if (parsed.allowHome || parsed.cwd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const home = homedir();
|
||||
if (!home) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizePath = (value: string) => {
|
||||
const resolved = resolve(value);
|
||||
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
||||
};
|
||||
|
||||
const cwd = normalizePath(process.cwd());
|
||||
const normalizedHome = normalizePath(home);
|
||||
if (cwd !== normalizedHome) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDirectory = async (path: string) => {
|
||||
try {
|
||||
const stat = await Bun.file(path).stat();
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const candidates = [join(home, "tmp"), "/tmp", "/var/tmp"];
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
if (!(await isDirectory(candidate))) {
|
||||
continue;
|
||||
}
|
||||
process.chdir(candidate);
|
||||
return;
|
||||
} catch {
|
||||
// Try next candidate.
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const fallback = tmpdir();
|
||||
if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
|
||||
process.chdir(fallback);
|
||||
}
|
||||
} catch {
|
||||
// Ignore fallback errors.
|
||||
}
|
||||
}
|
||||
|
||||
/** Discover SYSTEM.md file if no CLI system prompt was provided */
|
||||
function discoverSystemPromptFile(): string | undefined {
|
||||
// Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
|
||||
@@ -318,6 +373,10 @@ export async function main(args: string[]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseArgs(args);
|
||||
time("parseArgs");
|
||||
await maybeAutoChdir(parsed);
|
||||
|
||||
// Run migrations (pass cwd for project-local migrations)
|
||||
const { migratedAuthProviders: migratedProviders, deprecationWarnings } = await runMigrations(process.cwd());
|
||||
|
||||
@@ -326,9 +385,6 @@ export async function main(args: string[]) {
|
||||
const modelRegistry = await discoverModels(authStorage);
|
||||
time("discoverModels");
|
||||
|
||||
const parsed = parseArgs(args);
|
||||
time("parseArgs");
|
||||
|
||||
if (parsed.version) {
|
||||
console.log(VERSION);
|
||||
return;
|
||||
|
||||
+33
-19
@@ -38,30 +38,40 @@ export class ExtensionDashboard extends Container {
|
||||
private inspector: InspectorPanel;
|
||||
private settingsManager: SettingsManager | null;
|
||||
private cwd: string;
|
||||
private terminalHeight: number;
|
||||
|
||||
public onClose?: () => void;
|
||||
|
||||
constructor(cwd: string, settingsManager: SettingsManager | null = null) {
|
||||
constructor(cwd: string, settingsManager: SettingsManager | null = null, terminalHeight?: number) {
|
||||
super();
|
||||
this.cwd = cwd;
|
||||
this.settingsManager = settingsManager;
|
||||
this.terminalHeight = terminalHeight ?? process.stdout.rows ?? 24;
|
||||
const disabledIds = settingsManager?.getDisabledExtensions() ?? [];
|
||||
this.state = createInitialState(cwd, disabledIds);
|
||||
|
||||
// Calculate max visible items based on terminal height
|
||||
// Reserve ~10 lines for header, tabs, help text, borders
|
||||
const maxVisible = Math.max(5, Math.floor((this.terminalHeight - 10) / 2));
|
||||
|
||||
// Create main list - always focused
|
||||
this.mainList = new ExtensionList(this.state.searchFiltered, {
|
||||
onSelectionChange: (ext) => {
|
||||
this.state.selected = ext;
|
||||
this.inspector.setExtension(ext);
|
||||
this.mainList = new ExtensionList(
|
||||
this.state.searchFiltered,
|
||||
{
|
||||
onSelectionChange: (ext) => {
|
||||
this.state.selected = ext;
|
||||
this.inspector.setExtension(ext);
|
||||
},
|
||||
onToggle: (extensionId, enabled) => {
|
||||
this.handleExtensionToggle(extensionId, enabled);
|
||||
},
|
||||
onMasterToggle: (providerId) => {
|
||||
this.handleProviderToggle(providerId);
|
||||
},
|
||||
masterSwitchProvider: this.getActiveProviderId(),
|
||||
},
|
||||
onToggle: (extensionId, enabled) => {
|
||||
this.handleExtensionToggle(extensionId, enabled);
|
||||
},
|
||||
onMasterToggle: (providerId) => {
|
||||
this.handleProviderToggle(providerId);
|
||||
},
|
||||
masterSwitchProvider: this.getActiveProviderId(),
|
||||
});
|
||||
maxVisible,
|
||||
);
|
||||
this.mainList.setFocused(true);
|
||||
|
||||
// Create inspector
|
||||
@@ -91,9 +101,10 @@ export class ExtensionDashboard extends Container {
|
||||
this.addChild(new Text(this.renderTabBar(), 0, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Help text
|
||||
// 2-column body
|
||||
this.addChild(new TwoColumnBody(this.mainList, this.inspector));
|
||||
// 2-column body with height limit
|
||||
// Reserve ~8 lines for header, tabs, help text, borders
|
||||
const bodyMaxHeight = Math.max(5, this.terminalHeight - 8);
|
||||
this.addChild(new TwoColumnBody(this.mainList, this.inspector, bodyMaxHeight));
|
||||
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.fg("dim", " ↑/↓: navigate Space: toggle Tab: next provider Esc: close"), 0, 0));
|
||||
@@ -262,10 +273,12 @@ export class ExtensionDashboard extends Container {
|
||||
class TwoColumnBody implements Component {
|
||||
private leftPane: ExtensionList;
|
||||
private rightPane: InspectorPanel;
|
||||
private maxHeight: number;
|
||||
|
||||
constructor(left: ExtensionList, right: InspectorPanel) {
|
||||
constructor(left: ExtensionList, right: InspectorPanel, maxHeight: number) {
|
||||
this.leftPane = left;
|
||||
this.rightPane = right;
|
||||
this.maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
@@ -275,11 +288,12 @@ class TwoColumnBody implements Component {
|
||||
const leftLines = this.leftPane.render(leftWidth);
|
||||
const rightLines = this.rightPane.render(rightWidth);
|
||||
|
||||
const maxLines = Math.max(leftLines.length, rightLines.length);
|
||||
// Limit to maxHeight lines
|
||||
const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
||||
const combined: string[] = [];
|
||||
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
||||
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
for (let i = 0; i < numLines; i++) {
|
||||
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
||||
const leftPadded = left + " ".repeat(Math.max(0, leftWidth - visibleWidth(left)));
|
||||
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
||||
|
||||
+15
-8
@@ -31,7 +31,7 @@ export interface ExtensionListCallbacks {
|
||||
masterSwitchProvider?: string | null;
|
||||
}
|
||||
|
||||
const MAX_VISIBLE = 25;
|
||||
const DEFAULT_MAX_VISIBLE = 15;
|
||||
|
||||
/** Flattened list item for rendering */
|
||||
type ListItem =
|
||||
@@ -48,14 +48,21 @@ export class ExtensionList implements Component {
|
||||
private focused = false;
|
||||
private callbacks: ExtensionListCallbacks;
|
||||
private masterSwitchProvider: string | null = null;
|
||||
private maxVisible: number;
|
||||
|
||||
constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}) {
|
||||
constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}, maxVisible?: number) {
|
||||
this.extensions = extensions;
|
||||
this.callbacks = callbacks;
|
||||
this.masterSwitchProvider = callbacks.masterSwitchProvider ?? null;
|
||||
this.maxVisible = maxVisible ?? DEFAULT_MAX_VISIBLE;
|
||||
this.rebuildList();
|
||||
}
|
||||
|
||||
setMaxVisible(maxVisible: number): void {
|
||||
this.maxVisible = maxVisible;
|
||||
this.clampSelection();
|
||||
}
|
||||
|
||||
setExtensions(extensions: Extension[]): void {
|
||||
this.extensions = extensions;
|
||||
this.rebuildList();
|
||||
@@ -126,7 +133,7 @@ export class ExtensionList implements Component {
|
||||
|
||||
// Calculate visible range
|
||||
const startIdx = this.scrollOffset;
|
||||
const endIdx = Math.min(startIdx + MAX_VISIBLE, this.listItems.length);
|
||||
const endIdx = Math.min(startIdx + this.maxVisible, this.listItems.length);
|
||||
|
||||
// Render visible items
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
@@ -143,7 +150,7 @@ export class ExtensionList implements Component {
|
||||
}
|
||||
|
||||
// Scroll indicator
|
||||
if (this.listItems.length > MAX_VISIBLE) {
|
||||
if (this.listItems.length > this.maxVisible) {
|
||||
const indicator = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.listItems.length})`);
|
||||
lines.push(indicator);
|
||||
}
|
||||
@@ -388,8 +395,8 @@ export class ExtensionList implements Component {
|
||||
// Adjust scroll offset
|
||||
if (this.selectedIndex < this.scrollOffset) {
|
||||
this.scrollOffset = this.selectedIndex;
|
||||
} else if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
|
||||
this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
|
||||
} else if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
|
||||
this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,8 +477,8 @@ export class ExtensionList implements Component {
|
||||
private moveSelectionDown(): void {
|
||||
if (this.selectedIndex < this.listItems.length - 1) {
|
||||
this.selectedIndex++;
|
||||
if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
|
||||
this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
|
||||
if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
|
||||
this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
|
||||
}
|
||||
this.notifySelectionChange();
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ interface ScopedModelItem {
|
||||
thinkingLevel: string;
|
||||
}
|
||||
|
||||
type ModelRole = "default" | "smol" | "slow";
|
||||
type ModelRole = "default" | "smol" | "slow" | "temporary";
|
||||
|
||||
interface MenuAction {
|
||||
label: string;
|
||||
@@ -75,6 +75,7 @@ export class ModelSelectorComponent extends Container {
|
||||
private errorMessage?: string;
|
||||
private tui: TUI;
|
||||
private scopedModels: ReadonlyArray<ScopedModelItem>;
|
||||
private temporaryOnly: boolean;
|
||||
|
||||
// Tab state
|
||||
private providers: string[] = [ALL_TAB];
|
||||
@@ -92,6 +93,7 @@ export class ModelSelectorComponent extends Container {
|
||||
scopedModels: ReadonlyArray<ScopedModelItem>,
|
||||
onSelect: (model: Model<any>, role: string) => void,
|
||||
onCancel: () => void,
|
||||
options?: { temporaryOnly?: boolean },
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -102,6 +104,7 @@ export class ModelSelectorComponent extends Container {
|
||||
this.scopedModels = scopedModels;
|
||||
this.onSelectCallback = onSelect;
|
||||
this.onCancelCallback = onCancel;
|
||||
this.temporaryOnly = options?.temporaryOnly ?? false;
|
||||
|
||||
// Load current role assignments from settings
|
||||
this._loadRoleModels();
|
||||
@@ -483,10 +486,16 @@ export class ModelSelectorComponent extends Container {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter - open context menu
|
||||
// Enter - open context menu or select directly in temporary mode
|
||||
if (isEnter(keyData)) {
|
||||
if (this.filteredModels[this.selectedIndex]) {
|
||||
this.openMenu();
|
||||
const selectedModel = this.filteredModels[this.selectedIndex];
|
||||
if (selectedModel) {
|
||||
if (this.temporaryOnly) {
|
||||
// In temporary mode, skip menu and select directly
|
||||
this.handleSelect(selectedModel.model, "temporary");
|
||||
} else {
|
||||
this.openMenu();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -536,6 +545,12 @@ export class ModelSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
private handleSelect(model: Model<any>, role: ModelRole): void {
|
||||
// For temporary role, don't save to settings - just notify caller
|
||||
if (role === "temporary") {
|
||||
this.onSelectCallback(model, role);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to settings
|
||||
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
||||
|
||||
|
||||
@@ -942,9 +942,9 @@ export class InteractiveMode {
|
||||
this.editor.onCtrlD = () => this.handleCtrlD();
|
||||
this.editor.onCtrlZ = () => this.handleCtrlZ();
|
||||
this.editor.onShiftTab = () => this.cycleThinkingLevel();
|
||||
this.editor.onCtrlP = () => this.cycleModel("forward");
|
||||
this.editor.onShiftCtrlP = () => this.cycleModel("backward");
|
||||
this.editor.onCtrlY = () => this.cycleRoleModel();
|
||||
this.editor.onCtrlP = () => this.cycleRoleModel();
|
||||
this.editor.onShiftCtrlP = () => this.cycleRoleModel({ temporary: true });
|
||||
this.editor.onCtrlY = () => this.showModelSelector({ temporaryOnly: true });
|
||||
|
||||
// Global debug handler on TUI (works regardless of focus)
|
||||
this.ui.onDebug = () => this.handleDebugCommand();
|
||||
@@ -952,9 +952,6 @@ export class InteractiveMode {
|
||||
this.editor.onCtrlO = () => this.toggleToolOutputExpansion();
|
||||
this.editor.onCtrlT = () => this.toggleThinkingBlockVisibility();
|
||||
this.editor.onCtrlG = () => this.openExternalEditor();
|
||||
this.editor.onCtrlY = () => {
|
||||
void this.toggleVoiceListening();
|
||||
};
|
||||
this.editor.onQuestionMark = () => this.handleHotkeysCommand();
|
||||
this.editor.onCtrlV = () => this.handleImagePaste();
|
||||
|
||||
@@ -992,6 +989,14 @@ export class InteractiveMode {
|
||||
private setupEditorSubmitHandler(): void {
|
||||
this.editor.onSubmit = async (text: string) => {
|
||||
text = text.trim();
|
||||
|
||||
// Empty submit while streaming with queued messages: flush queues immediately
|
||||
if (!text && this.session.isStreaming && this.session.queuedMessageCount > 0) {
|
||||
// Abort current stream and let queued messages be processed
|
||||
await this.session.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text) return;
|
||||
|
||||
// Handle slash commands
|
||||
@@ -1995,27 +2000,9 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private async cycleModel(direction: "forward" | "backward"): Promise<void> {
|
||||
private async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
|
||||
try {
|
||||
const result = await this.session.cycleModel(direction);
|
||||
if (result === undefined) {
|
||||
const msg = this.session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available";
|
||||
this.showStatus(msg);
|
||||
} else {
|
||||
this.statusLine.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
const thinkingStr =
|
||||
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
||||
this.showStatus(`Switched to ${result.model.name || result.model.id}${thinkingStr}`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.showError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
private async cycleRoleModel(): Promise<void> {
|
||||
try {
|
||||
const result = await this.session.cycleRoleModels(["slow", "default", "smol"]);
|
||||
const result = await this.session.cycleRoleModels(["slow", "default", "smol"], options);
|
||||
if (!result) {
|
||||
this.showStatus("Only one role model available");
|
||||
return;
|
||||
@@ -2026,7 +2013,8 @@ export class InteractiveMode {
|
||||
const roleLabel = result.role === "default" ? "default" : result.role;
|
||||
const thinkingStr =
|
||||
result.model.reasoning && result.thinkingLevel !== "off" ? ` (thinking: ${result.thinkingLevel})` : "";
|
||||
this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}`);
|
||||
const tempLabel = options?.temporary ? " (temporary)" : "";
|
||||
this.showStatus(`Switched to ${roleLabel}: ${result.model.name || result.model.id}${thinkingStr}${tempLabel}`);
|
||||
} catch (error) {
|
||||
this.showError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
@@ -2256,7 +2244,7 @@ export class InteractiveMode {
|
||||
*/
|
||||
private showExtensionsDashboard(): void {
|
||||
this.showSelector((done) => {
|
||||
const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager);
|
||||
const dashboard = new ExtensionDashboard(process.cwd(), this.settingsManager, this.ui.terminal.rows);
|
||||
dashboard.onClose = () => {
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
@@ -2380,7 +2368,7 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private showModelSelector(): void {
|
||||
private showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
||||
this.showSelector((done) => {
|
||||
const selector = new ModelSelectorComponent(
|
||||
this.ui,
|
||||
@@ -2390,24 +2378,36 @@ export class InteractiveMode {
|
||||
this.session.scopedModels,
|
||||
async (model, role) => {
|
||||
try {
|
||||
// Only update agent state for default role
|
||||
if (role === "default") {
|
||||
if (role === "temporary") {
|
||||
// Temporary: update agent state but don't persist to settings
|
||||
await this.session.setModelTemporary(model);
|
||||
this.statusLine.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
this.showStatus(`Temporary model: ${model.id}`);
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
} else if (role === "default") {
|
||||
// Default: update agent state and persist
|
||||
await this.session.setModel(model, role);
|
||||
this.statusLine.invalidate();
|
||||
this.updateEditorBorderColor();
|
||||
this.showStatus(`Default model: ${model.id}`);
|
||||
// Don't call done() - selector stays open for role assignment
|
||||
} else {
|
||||
// Other roles (smol, slow): just update settings, not current model
|
||||
const roleLabel = role === "smol" ? "Smol" : role;
|
||||
this.showStatus(`${roleLabel} model: ${model.id}`);
|
||||
// Don't call done() - selector stays open
|
||||
}
|
||||
// For other roles (small), just show status - settings already updated by selector
|
||||
const roleLabel = role === "default" ? "Default" : role === "smol" ? "Smol" : role;
|
||||
this.showStatus(`${roleLabel} model: ${model.id}`);
|
||||
} catch (error) {
|
||||
this.showError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
// Don't call done() - selector stays open
|
||||
},
|
||||
() => {
|
||||
done();
|
||||
this.ui.requestRender();
|
||||
},
|
||||
options,
|
||||
);
|
||||
return { component: selector, focus: selector };
|
||||
});
|
||||
@@ -2995,8 +2995,10 @@ export class InteractiveMode {
|
||||
| \`Ctrl+D\` | Exit (when editor is empty) |
|
||||
| \`Ctrl+Z\` | Suspend to background |
|
||||
| \`Shift+Tab\` | Cycle thinking level |
|
||||
| \`Ctrl+P\` | Cycle models |
|
||||
| \`Ctrl+Y\` | Cycle role models (slow/default/smol) |
|
||||
| \`Ctrl+P\` | Cycle role models (slow/default/smol) |
|
||||
| \`Shift+Ctrl+P\` | Cycle role models (temporary) |
|
||||
| \`Ctrl+Y\` | Select model (temporary) |
|
||||
| \`Ctrl+L\` | Select model (set roles) |
|
||||
| \`Ctrl+O\` | Toggle tool output expansion |
|
||||
| \`Ctrl+T\` | Toggle thinking block visibility |
|
||||
| \`Ctrl+G\` | Edit message in external editor |
|
||||
|
||||
Reference in New Issue
Block a user