From 046d30e63097fece44a541c7970379d44f81a677 Mon Sep 17 00:00:00 2001 From: Ingo Wolf Date: Sun, 13 Sep 2026 00:40:00 +1000 Subject: [PATCH] feat(coding-agent): make agent unstoppable, add agent-only quit tool Remove all user-facing exit/stop paths: double Ctrl+C exit, Ctrl+D exit, /quit command, app.exit keybinding, and Escape aborts for streaming, bash, retry, compaction, and branch summarization. The agent can now quit the app only via the new quit tool. --- packages/coding-agent/CHANGELOG.md | 6 ++ packages/coding-agent/docs/keybindings.md | 3 +- packages/coding-agent/docs/usage.md | 2 - .../coding-agent/src/core/agent-session.ts | 2 +- .../src/core/extensions/runner.ts | 1 - packages/coding-agent/src/core/keybindings.ts | 5 +- .../coding-agent/src/core/slash-commands.ts | 1 - packages/coding-agent/src/core/tools/index.ts | 18 +++- packages/coding-agent/src/core/tools/quit.ts | 30 ++++++ .../interactive/components/custom-editor.ts | 13 +-- .../src/modes/interactive/interactive-mode.ts | 102 +++--------------- 11 files changed, 72 insertions(+), 111 deletions(-) create mode 100644 packages/coding-agent/src/core/tools/quit.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 56fb86ee..0d0b9aa9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,10 +5,16 @@ ### Added - Added silent background self-updates on every startup for global npm, pnpm, yarn, and bun installs. Pi checks npm for a newer release and updates in the background without blocking startup or showing output. +- Added a `quit` tool that lets the agent gracefully shut down the application. + +### Changed + +- Removed all user-facing ways to stop or exit the agent: double Ctrl+C no longer exits, Ctrl+D no longer exits, Escape no longer aborts streaming/bash/retry/compaction, and the `/quit` command is gone. The agent can quit via the `quit` tool. ### Removed - Removed interactive slash commands `/export`, `/import`, `/session`, `/changelog`, `/debug`, `/arminsayshi`, and `/dementedelves`. +- Removed the `app.exit` keybinding (Ctrl+D exit). ## [0.83.0] - 2026-09-01 diff --git a/packages/coding-agent/docs/keybindings.md b/packages/coding-agent/docs/keybindings.md index a73be3a6..0f154470 100644 --- a/packages/coding-agent/docs/keybindings.md +++ b/packages/coding-agent/docs/keybindings.md @@ -82,9 +82,8 @@ Modifier combinations: `ctrl+shift+x`, `alt+ctrl+x`, `ctrl+shift+alt+x`, `ctrl+1 | Keybinding id | Default | Description | |--------|---------|-------------| -| `app.interrupt` | `escape` | Cancel / abort | +| `app.interrupt` | `escape` | Cancel autocomplete | | `app.clear` | `ctrl+c` | Clear editor | -| `app.exit` | `ctrl+d` | Exit (when editor empty) | | `app.suspend` | `ctrl+z` (none on Windows) | Suspend to background | | `app.editor.external` | `ctrl+g` | Open in external editor (`externalEditor`, `$VISUAL`, `$EDITOR`, Notepad on Windows, or `nano` elsewhere) | | `app.clipboard.pasteImage` | `ctrl+v` (`alt+v` on Windows) | Paste image from clipboard | diff --git a/packages/coding-agent/docs/usage.md b/packages/coding-agent/docs/usage.md index 9882f046..9be2ff40 100644 --- a/packages/coding-agent/docs/usage.md +++ b/packages/coding-agent/docs/usage.md @@ -50,7 +50,6 @@ Type `/` in the editor to open command completion. Extensions can register custo | `/reload` | Reload keybindings, extensions, skills, prompts, and context files | | `/hotkeys` | Show all keyboard shortcuts | | `/changelog` | Display version history | -| `/quit` | Quit pi | ## Message Queue @@ -58,7 +57,6 @@ You can submit messages while the agent is still working: - **Enter** queues a steering message, delivered after the current assistant turn finishes executing its tool calls. - **Alt+Enter** queues a follow-up message, delivered after the agent finishes all work. -- **Escape** aborts and restores queued messages to the editor. - **Alt+Up** retrieves queued messages back to the editor. On Windows Terminal, Alt+Enter is fullscreen by default. Remap it as described in [Terminal setup](terminal-setup.md) if you want pi to receive the shortcut. diff --git a/packages/coding-agent/src/core/agent-session.ts b/packages/coding-agent/src/core/agent-session.ts index 667428d3..ab4bc04a 100644 --- a/packages/coding-agent/src/core/agent-session.ts +++ b/packages/coding-agent/src/core/agent-session.ts @@ -2408,7 +2408,7 @@ export class AgentSession { const defaultActiveToolNames = this._baseToolsOverride ? Object.keys(this._baseToolsOverride) - : ["read", "bash", "edit", "write"]; + : ["read", "bash", "edit", "write", "quit"]; const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames; this._refreshToolRegistry({ activeToolNames: baseActiveToolNames, diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index ff7255b4..3fbef9f8 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -67,7 +67,6 @@ import type { const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [ "app.interrupt", "app.clear", - "app.exit", "app.suspend", "app.thinking.cycle", "app.tools.expand", diff --git a/packages/coding-agent/src/core/keybindings.ts b/packages/coding-agent/src/core/keybindings.ts index a92454b1..160f62bf 100644 --- a/packages/coding-agent/src/core/keybindings.ts +++ b/packages/coding-agent/src/core/keybindings.ts @@ -13,7 +13,6 @@ import { getAgentDir } from "../config.ts"; export interface AppKeybindings { "app.interrupt": true; "app.clear": true; - "app.exit": true; "app.suspend": true; "app.thinking.cycle": true; "app.tools.expand": true; @@ -53,9 +52,8 @@ declare module "@southbag/code-tui" { export const KEYBINDINGS = { ...TUI_KEYBINDINGS, - "app.interrupt": { defaultKeys: "escape", description: "Cancel or abort" }, + "app.interrupt": { defaultKeys: "escape", description: "Cancel autocomplete" }, "app.clear": { defaultKeys: "ctrl+c", description: "Clear editor" }, - "app.exit": { defaultKeys: "ctrl+d", description: "Exit when editor is empty" }, "app.suspend": { defaultKeys: process.platform === "win32" ? [] : "ctrl+z", description: "Suspend to background", @@ -193,7 +191,6 @@ const KEYBINDING_NAME_MIGRATIONS = { selectCancel: "tui.select.cancel", interrupt: "app.interrupt", clear: "app.clear", - exit: "app.exit", suspend: "app.suspend", cycleThinkingLevel: "app.thinking.cycle", expandTools: "app.tools.expand", diff --git a/packages/coding-agent/src/core/slash-commands.ts b/packages/coding-agent/src/core/slash-commands.ts index b5c2acdf..c6aae5a5 100644 --- a/packages/coding-agent/src/core/slash-commands.ts +++ b/packages/coding-agent/src/core/slash-commands.ts @@ -28,5 +28,4 @@ export const BUILTIN_SLASH_COMMANDS: ReadonlyArray = [ { name: "resume", description: "Resume a different session" }, { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" }, { name: "logout", description: `Log out and quit ${APP_NAME}` }, - { name: "quit", description: `Quit ${APP_NAME}` }, ]; diff --git a/packages/coding-agent/src/core/tools/index.ts b/packages/coding-agent/src/core/tools/index.ts index 7e4165e1..007a8397 100644 --- a/packages/coding-agent/src/core/tools/index.ts +++ b/packages/coding-agent/src/core/tools/index.ts @@ -42,6 +42,11 @@ export { type LsToolInput, type LsToolOptions, } from "./ls.ts"; +export { + createQuitTool, + createQuitToolDefinition, + type QuitToolInput, +} from "./quit.ts"; export { createReadTool, createReadToolDefinition, @@ -75,13 +80,14 @@ import { createEditTool, createEditToolDefinition, type EditToolOptions } from " import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.ts"; import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.ts"; import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.ts"; +import { createQuitTool, createQuitToolDefinition } from "./quit.ts"; import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.ts"; import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.ts"; export type Tool = AgentTool; export type ToolDef = ToolDefinition; -export type ToolName = "read" | "bash" | "edit" | "write" | "grep" | "find" | "ls"; -export const allToolNames: Set = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]); +export type ToolName = "read" | "bash" | "edit" | "write" | "grep" | "find" | "ls" | "quit"; +export const allToolNames: Set = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "quit"]); export interface ToolsOptions { read?: ReadToolOptions; @@ -109,6 +115,8 @@ export function createToolDefinition(toolName: ToolName, cwd: string, options?: return createFindToolDefinition(cwd, options?.find); case "ls": return createLsToolDefinition(cwd, options?.ls); + case "quit": + return createQuitToolDefinition(); default: throw new Error(`Unknown tool name: ${toolName}`); } @@ -130,6 +138,8 @@ export function createTool(toolName: ToolName, cwd: string, options?: ToolsOptio return createFindTool(cwd, options?.find); case "ls": return createLsTool(cwd, options?.ls); + case "quit": + return createQuitTool(); default: throw new Error(`Unknown tool name: ${toolName}`); } @@ -141,6 +151,7 @@ export function createCodingToolDefinitions(cwd: string, options?: ToolsOptions) createBashToolDefinition(cwd, options?.bash), createEditToolDefinition(cwd, options?.edit), createWriteToolDefinition(cwd, options?.write), + createQuitToolDefinition(), ]; } @@ -162,6 +173,7 @@ export function createAllToolDefinitions(cwd: string, options?: ToolsOptions): R grep: createGrepToolDefinition(cwd, options?.grep), find: createFindToolDefinition(cwd, options?.find), ls: createLsToolDefinition(cwd, options?.ls), + quit: createQuitToolDefinition(), }; } @@ -171,6 +183,7 @@ export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] { createBashTool(cwd, options?.bash), createEditTool(cwd, options?.edit), createWriteTool(cwd, options?.write), + createQuitTool(), ]; } @@ -192,5 +205,6 @@ export function createAllTools(cwd: string, options?: ToolsOptions): Record; + +export function createQuitToolDefinition(): ToolDefinition { + return { + name: "quit", + label: "quit", + description: `Quit ${APP_NAME}. Gracefully shuts down the application and terminates the process.`, + promptSnippet: `Quit ${APP_NAME}`, + parameters: quitSchema, + async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) { + process.kill(process.pid, "SIGTERM"); + return { + content: [{ type: "text", text: `Quitting ${APP_NAME}...` }], + details: undefined, + }; + }, + }; +} + +export function createQuitTool(): AgentTool { + return wrapToolDefinition(createQuitToolDefinition()); +} diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 9bccd263..4567e281 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -10,7 +10,6 @@ export class CustomEditor extends Editor { // Special handlers that can be dynamically replaced public onEscape?: () => void; - public onCtrlD?: () => void; public onPasteImage?: () => void; /** Handler for extension-registered shortcuts. Returns true if handled. */ public onExtensionShortcut?: (data: string) => boolean; @@ -56,19 +55,9 @@ export class CustomEditor extends Editor { return; } - // Exit (Ctrl+D) - only when editor is empty - if (this.keybindings.matches(data, "app.exit")) { - if (this.getText().length === 0) { - const handler = this.onCtrlD ?? this.actionHandlers.get("app.exit"); - if (handler) handler(); - return; - } - // Fall through to editor handling for delete-char-forward when not empty - } - // Check all other app actions for (const [action, handler] of this.actionHandlers) { - if (action !== "app.interrupt" && action !== "app.exit" && this.keybindings.matches(data, action)) { + if (action !== "app.interrupt" && this.keybindings.matches(data, action)) { handler(); return; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 3af1b1c1..efde1a96 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -79,7 +79,7 @@ import { ExtensionEditorComponent } from "./components/extension-editor.ts"; import { ExtensionInputComponent } from "./components/extension-input.ts"; import { ExtensionSelectorComponent } from "./components/extension-selector.ts"; import { FooterComponent } from "./components/footer.ts"; -import { formatKeyText, keyDisplayText, keyText } from "./components/keybinding-hints.ts"; +import { formatKeyText, keyDisplayText } from "./components/keybinding-hints.ts"; import { SessionSelectorComponent } from "./components/session-selector.ts"; import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.ts"; import { ToolExecutionComponent } from "./components/tool-execution.ts"; @@ -217,7 +217,6 @@ export class InteractiveMode { private readonly defaultHiddenThinkingLabel = "Thinking..."; private hiddenThinkingLabel = this.defaultHiddenThinkingLabel; - private lastSigintTime = 0; private lastEscapeTime = 0; private changelogMarkdown: string | undefined = undefined; private startupNoticesShown = false; @@ -257,12 +256,10 @@ export class InteractiveMode { // Auto-compaction state private autoCompactionLoader: Loader | undefined = undefined; - private autoCompactionEscapeHandler?: () => void; // Auto-retry state private retryLoader: Loader | undefined = undefined; private retryCountdown: CountdownTimer | undefined = undefined; - private retryEscapeHandler?: () => void; // Messages queued while compaction is running private compactionQueuedMessages: CompactionQueuedMessage[] = []; @@ -302,9 +299,6 @@ export class InteractiveMode { private get session(): AgentSession { return this.runtimeHost.session; } - private get agent() { - return this.session.agent; - } private get sessionManager() { return this.session.sessionManager; } @@ -1321,7 +1315,7 @@ export class InteractiveMode { uiContext, mode: "tui", abortHandler: () => { - this.restoreQueuedMessagesToEditor({ abort: true }); + this.restoreQueuedMessagesToEditor(); }, commandContextActions: { waitForIdle: () => this.session.agent.waitForIdle(), @@ -1477,7 +1471,7 @@ export class InteractiveMode { isProjectTrusted: () => this.settingsManager.isProjectTrusted(), signal: this.session.agent.signal, abort: () => { - this.restoreQueuedMessagesToEditor({ abort: true }); + this.restoreQueuedMessagesToEditor(); }, hasPendingMessages: () => this.session.pendingMessageCount > 0, shutdown: () => { @@ -1661,7 +1655,7 @@ export class InteractiveMode { this.workingVisible = true; this.setWorkingIndicator(); if (this.loadingAnimation) { - this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`); + this.loadingAnimation.setMessage(this.defaultWorkingMessage); } this.setHiddenThinkingLabel(); } @@ -2082,9 +2076,6 @@ export class InteractiveMode { if (!customEditor.onEscape) { customEditor.onEscape = () => this.defaultEditor.onEscape?.(); } - if (!customEditor.onCtrlD) { - customEditor.onCtrlD = () => this.defaultEditor.onCtrlD?.(); - } if (!customEditor.onPasteImage) { customEditor.onPasteImage = () => this.defaultEditor.onPasteImage?.(); } @@ -2230,11 +2221,7 @@ export class InteractiveMode { // Set up handlers on defaultEditor - they use this.editor for text access // so they work correctly regardless of which editor is active this.defaultEditor.onEscape = () => { - if (this.session.isStreaming) { - this.restoreQueuedMessagesToEditor({ abort: true }); - } else if (this.session.isBashRunning) { - this.session.abortBash(); - } else if (this.isBashMode) { + if (this.isBashMode) { this.editor.setText(""); this.isBashMode = false; this.updateEditorBorderColor(); @@ -2258,8 +2245,7 @@ export class InteractiveMode { }; // Register app action handlers - this.defaultEditor.onAction("app.clear", () => this.handleCtrlC()); - this.defaultEditor.onCtrlD = () => this.handleCtrlD(); + this.defaultEditor.onAction("app.clear", () => this.clearEditor()); this.defaultEditor.onAction("app.suspend", () => this.handleCtrlZ()); this.defaultEditor.onAction("app.thinking.cycle", () => this.cycleThinkingLevel()); @@ -2376,11 +2362,6 @@ export class InteractiveMode { this.editor.setText(""); return; } - if (text === "/quit") { - this.editor.setText(""); - await this.shutdown(); - return; - } if (text === "/logout") { this.editor.setText(""); this.session.modelRegistry.authStorage.remove("southbag-agent"); @@ -2394,7 +2375,7 @@ export class InteractiveMode { const command = isExcluded ? text.slice(2).trim() : text.slice(1).trim(); if (command) { if (this.session.isBashRunning) { - this.showWarning("A bash command is already running. Press Esc to cancel it first."); + this.showWarning("A bash command is already running. Wait for it to finish."); this.editor.setText(text); return; } @@ -2463,10 +2444,6 @@ export class InteractiveMode { } // Restore main escape handler if retry handler is still active // (retry success event fires later, but we need main handler now) - if (this.retryEscapeHandler) { - this.defaultEditor.onEscape = this.retryEscapeHandler; - this.retryEscapeHandler = undefined; - } if (this.retryCountdown) { this.retryCountdown.dispose(); this.retryCountdown = undefined; @@ -2664,16 +2641,11 @@ export class InteractiveMode { this.ui.terminal.setProgress(true); } // Keep editor active; submissions are queued during compaction. - this.autoCompactionEscapeHandler = this.defaultEditor.onEscape; - this.defaultEditor.onEscape = () => { - this.session.abortCompaction(); - }; this.statusContainer.clear(); - const cancelHint = `(${keyText("app.interrupt")} to cancel)`; const label = event.reason === "manual" - ? `Compacting context... ${cancelHint}` - : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting... ${cancelHint}`; + ? "Compacting context..." + : `${event.reason === "overflow" ? "Context overflow detected, " : ""}Auto-compacting...`; this.autoCompactionLoader = new Loader( this.ui, (spinner) => theme.fg("accent", spinner), @@ -2689,10 +2661,6 @@ export class InteractiveMode { if (this.settingsManager.getShowTerminalProgress()) { this.ui.terminal.setProgress(false); } - if (this.autoCompactionEscapeHandler) { - this.defaultEditor.onEscape = this.autoCompactionEscapeHandler; - this.autoCompactionEscapeHandler = undefined; - } if (this.autoCompactionLoader) { this.autoCompactionLoader.stop(); this.autoCompactionLoader = undefined; @@ -2729,16 +2697,11 @@ export class InteractiveMode { } case "auto_retry_start": { - // Set up escape to abort retry - this.retryEscapeHandler = this.defaultEditor.onEscape; - this.defaultEditor.onEscape = () => { - this.session.abortRetry(); - }; // Show retry indicator this.statusContainer.clear(); this.retryCountdown?.dispose(); const retryMessage = (seconds: number) => - `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s... (${keyText("app.interrupt")} to cancel)`; + `Retrying (${event.attempt}/${event.maxAttempts}) in ${seconds}s...`; this.retryLoader = new Loader( this.ui, (spinner) => theme.fg("warning", spinner), @@ -2761,11 +2724,6 @@ export class InteractiveMode { } case "auto_retry_end": { - // Restore escape handler - if (this.retryEscapeHandler) { - this.defaultEditor.onEscape = this.retryEscapeHandler; - this.retryEscapeHandler = undefined; - } if (this.retryCountdown) { this.retryCountdown.dispose(); this.retryCountdown = undefined; @@ -3037,21 +2995,6 @@ export class InteractiveMode { // Key handlers // ========================================================================= - private handleCtrlC(): void { - const now = Date.now(); - if (now - this.lastSigintTime < 500) { - void this.shutdown(); - } else { - this.clearEditor(); - this.lastSigintTime = now; - } - } - - private handleCtrlD(): void { - // Only called when editor is empty (enforced by CustomEditor) - void this.shutdown(); - } - /** * Gracefully shutdown the agent. * Stops the TUI before emitting shutdown events so extension UI cleanup cannot @@ -3081,7 +3024,7 @@ export class InteractiveMode { process.exit(0); } - // Interactive quit (Ctrl+D, Ctrl+C, /quit, extension shutdown()). Stop the + // Interactive quit (quit tool, /logout, extension shutdown()). Stop the // TUI before emitting shutdown events so extension UI cleanup cannot repaint // the final frame while the process is exiting. // Drain any in-flight Kitty key release events before stopping. @@ -3462,14 +3405,11 @@ export class InteractiveMode { } } - private restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number { + private restoreQueuedMessagesToEditor(options?: { currentText?: string }): number { const { steering, followUp } = this.clearAllQueues(); const allQueued = [...steering, ...followUp]; if (allQueued.length === 0) { this.updatePendingMessagesDisplay(); - if (options?.abort) { - this.agent.abort(); - } return 0; } const queuedText = allQueued.join("\n\n"); @@ -3477,9 +3417,6 @@ export class InteractiveMode { const combinedText = [queuedText, currentText].filter((t) => t.trim()).join("\n\n"); this.editor.setText(combinedText); this.updatePendingMessagesDisplay(); - if (options?.abort) { - this.agent.abort(); - } return allQueued.length; } @@ -3733,20 +3670,16 @@ export class InteractiveMode { } } - // Set up escape handler and loader if summarizing + // Show loader if summarizing let summaryLoader: Loader | undefined; - const originalOnEscape = this.defaultEditor.onEscape; if (wantsSummary) { - this.defaultEditor.onEscape = () => { - this.session.abortBranchSummary(); - }; this.chatContainer.addChild(new Spacer(1)); summaryLoader = new Loader( this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), - `Summarizing branch... (${keyText("app.interrupt")} to cancel)`, + "Summarizing branch...", ); this.statusContainer.addChild(summaryLoader); this.ui.requestRender(); @@ -3784,7 +3717,6 @@ export class InteractiveMode { summaryLoader.stop(); this.statusContainer.clear(); } - this.defaultEditor.onEscape = originalOnEscape; } }, () => { @@ -4059,7 +3991,6 @@ export class InteractiveMode { // App keybindings const interrupt = this.getAppKeyDisplay("app.interrupt"); const clear = this.getAppKeyDisplay("app.clear"); - const exit = this.getAppKeyDisplay("app.exit"); const suspend = this.getAppKeyDisplay("app.suspend"); const cycleThinkingLevel = this.getAppKeyDisplay("app.thinking.cycle"); const expandTools = this.getAppKeyDisplay("app.tools.expand"); @@ -4098,9 +4029,8 @@ export class InteractiveMode { | Key | Action | |-----|--------| | \`${tab}\` | Path completion / accept autocomplete | -| \`${interrupt}\` | Cancel autocomplete / abort streaming | -| \`${clear}\` | Clear editor (first) / exit (second) | -| \`${exit}\` | Exit (when editor is empty) | +| \`${interrupt}\` | Cancel autocomplete | +| \`${clear}\` | Clear editor | | \`${suspend}\` | Suspend to background | | \`${cycleThinkingLevel}\` | Cycle thinking level | | \`${expandTools}\` | Toggle tool output expansion |