From d11ccd2e93e13587f6a59cd39bfea4e40f5c3530 Mon Sep 17 00:00:00 2001 From: can1357 Date: Thu, 8 Jan 2026 02:51:29 +0100 Subject: [PATCH] feat(coding-agent): added temp model controls and auto-chdir --- packages/coding-agent/CHANGELOG.md | 11 +++ packages/coding-agent/src/cli/args.ts | 4 + .../coding-agent/src/core/agent-session.ts | 31 +++++++- packages/coding-agent/src/core/tools/index.ts | 6 +- .../coding-agent/src/core/tools/lsp/index.ts | 8 -- packages/coding-agent/src/main.ts | 62 ++++++++++++++- .../extensions/extension-dashboard.ts | 52 ++++++++----- .../components/extensions/extension-list.ts | 23 ++++-- .../interactive/components/model-selector.ts | 23 +++++- .../src/modes/interactive/interactive-mode.ts | 76 ++++++++++--------- 10 files changed, 210 insertions(+), 86 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index de9fa409f..0d66f3d37 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index f361245d0..b6726817a 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -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 API key (defaults to env vars) --system-prompt System prompt (default: coding assistant prompt) --append-system-prompt Append text or file contents to the system prompt + --allow-home Allow starting in ~ without auto-switching to a temp dir --mode Output mode: text (default), json, or rpc --print, -p Non-interactive mode: process prompt and exit --continue, -c Continue previous session diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index ec9512def..a7e687a79 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -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): Promise { + 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 { + async cycleRoleModels( + roleOrder: string[], + options?: { temporary?: boolean }, + ): Promise { 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 }; } diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 0801d7c9c..f398684c1 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -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); diff --git a/packages/coding-agent/src/core/tools/lsp/index.ts b/packages/coding-agent/src/core/tools/lsp/index.ts index 1293e9185..951008b64 100644 --- a/packages/coding-agent/src/core/tools/lsp/index.ts +++ b/packages/coding-agent/src/core/tools/lsp/index.ts @@ -1393,11 +1393,3 @@ export function createLspTool(session: ToolSession): AgentTool null, - getSessionSpawns: () => null, -}); diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 439ba7241..543afa276 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -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 { + 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; diff --git a/packages/coding-agent/src/modes/interactive/components/extensions/extension-dashboard.ts b/packages/coding-agent/src/modes/interactive/components/extensions/extension-dashboard.ts index ab640c574..eac3f11e3 100644 --- a/packages/coding-agent/src/modes/interactive/components/extensions/extension-dashboard.ts +++ b/packages/coding-agent/src/modes/interactive/components/extensions/extension-dashboard.ts @@ -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); diff --git a/packages/coding-agent/src/modes/interactive/components/extensions/extension-list.ts b/packages/coding-agent/src/modes/interactive/components/extensions/extension-list.ts index 1282de739..aa75c193c 100644 --- a/packages/coding-agent/src/modes/interactive/components/extensions/extension-list.ts +++ b/packages/coding-agent/src/modes/interactive/components/extensions/extension-list.ts @@ -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(); } diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts index 8a1f34691..75849dfa7 100644 --- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts @@ -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; + private temporaryOnly: boolean; // Tab state private providers: string[] = [ALL_TAB]; @@ -92,6 +93,7 @@ export class ModelSelectorComponent extends Container { scopedModels: ReadonlyArray, onSelect: (model: Model, 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, 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}`); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6a7ce14f7..5b6fb1744 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -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 { + private async cycleRoleModel(options?: { temporary?: boolean }): Promise { 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 { - 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 |