From e7f8d9cf1ac64f18d196a9b60f8dc6cd4049ed37 Mon Sep 17 00:00:00 2001 From: Gaurav Ghosh Date: Wed, 8 Apr 2026 07:31:17 -0700 Subject: [PATCH] Revert "feat: Introduce an AI-driven interactive shell mode with new" This reverts commit 651ad63ed6daf4decf9071d5aa0bc9a4e715434d. --- packages/cli/src/config/config.ts | 1 - packages/cli/src/config/settingsSchema.ts | 20 -- packages/cli/src/ui/hooks/shellReducer.ts | 18 +- .../src/ui/hooks/useBackgroundShellManager.ts | 101 -------- .../cli/src/ui/hooks/useExecutionLifecycle.ts | 5 - packages/cli/src/ui/hooks/useGeminiStream.ts | 3 - packages/core/src/config/config.ts | 27 +- packages/core/src/prompts/promptProvider.ts | 1 - packages/core/src/prompts/snippets.ts | 16 +- .../src/services/shellExecutionService.ts | 41 ---- .../tools/definitions/base-declarations.ts | 12 - .../core/src/tools/definitions/coreTools.ts | 11 - .../dynamic-declaration-helpers.ts | 30 --- .../model-family-sets/default-legacy.ts | 2 - .../definitions/model-family-sets/gemini-3.ts | 2 - packages/core/src/tools/definitions/types.ts | 1 - packages/core/src/tools/read-shell.ts | 148 ----------- packages/core/src/tools/shell.test.ts | 6 +- packages/core/src/tools/shell.ts | 169 ++++++------- .../core/src/tools/shellOutputFormatter.ts | 128 ---------- packages/core/src/tools/tool-names.ts | 19 -- packages/core/src/tools/write-to-shell.ts | 230 ------------------ 22 files changed, 84 insertions(+), 907 deletions(-) delete mode 100644 packages/cli/src/ui/hooks/useBackgroundShellManager.ts delete mode 100644 packages/core/src/tools/read-shell.ts delete mode 100644 packages/core/src/tools/shellOutputFormatter.ts delete mode 100644 packages/core/src/tools/write-to-shell.ts diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 499b57b522..4e7e1db6f2 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1009,7 +1009,6 @@ export async function loadCliConfig( enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell, shellBackgroundCompletionBehavior: settings.tools?.shell ?.backgroundCompletionBehavior as string | undefined, - interactiveShellMode: settings.tools?.shell?.interactiveShellMode, shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout, enableShellOutputEfficiency: settings.tools?.shell?.enableShellOutputEfficiency ?? true, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index e654391566..c041aaa8c3 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1512,26 +1512,6 @@ const SETTINGS_SCHEMA = { { label: 'Notify', value: 'notify' }, ], }, - interactiveShellMode: { - type: 'enum', - label: 'Interactive Shell Mode', - category: 'Tools', - requiresRestart: true, - default: undefined as 'human' | 'ai' | 'off' | undefined, - description: oneLine` - Controls who can interact with backgrounded shell processes. - "human": user can Tab-focus and type into shells (default). - "ai": model gets write_to_shell/read_shell tools for TUI interaction. - "off": no interactive shell. - When set, overrides enableInteractiveShell. - `, - showInDialog: true, - options: [ - { value: 'human', label: 'Human (Tab to focus)' }, - { value: 'ai', label: 'AI (model-driven tools)' }, - { value: 'off', label: 'Off' }, - ], - }, pager: { type: 'string', label: 'Pager', diff --git a/packages/cli/src/ui/hooks/shellReducer.ts b/packages/cli/src/ui/hooks/shellReducer.ts index ea467fc327..0e9307259d 100644 --- a/packages/cli/src/ui/hooks/shellReducer.ts +++ b/packages/cli/src/ui/hooks/shellReducer.ts @@ -92,23 +92,7 @@ export function shellReducer( nextTasks.delete(action.pid); } nextTasks.set(action.pid, updatedTask); - - // Auto-hide panel when all tasks have exited - let nextVisible = state.isBackgroundTaskVisible; - if (action.update.status === 'exited') { - const hasRunning = Array.from(nextTasks.values()).some( - (s) => s.status === 'running', - ); - if (!hasRunning) { - nextVisible = false; - } - } - - return { - ...state, - backgroundTasks: nextTasks, - isBackgroundTaskVisible: nextVisible, - }; + return { ...state, backgroundTasks: nextTasks }; } case 'APPEND_TASK_OUTPUT': { const task = state.backgroundTasks.get(action.pid); diff --git a/packages/cli/src/ui/hooks/useBackgroundShellManager.ts b/packages/cli/src/ui/hooks/useBackgroundShellManager.ts deleted file mode 100644 index eb43ae1cfb..0000000000 --- a/packages/cli/src/ui/hooks/useBackgroundShellManager.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useState, useEffect, useMemo, useRef } from 'react'; -import { type BackgroundTask } from './shellReducer.js'; - -export interface BackgroundShellManagerProps { - backgroundTasks: Map; - backgroundTaskCount: number; - isBackgroundTaskVisible: boolean; - activePtyId: number | null | undefined; - embeddedShellFocused: boolean; - setEmbeddedShellFocused: (focused: boolean) => void; - terminalHeight: number; -} - -export function useBackgroundShellManager({ - backgroundTasks, - backgroundTaskCount, - isBackgroundTaskVisible, - activePtyId, - embeddedShellFocused, - setEmbeddedShellFocused, - terminalHeight, -}: BackgroundShellManagerProps) { - const [isBackgroundShellListOpen, setIsBackgroundShellListOpen] = - useState(false); - const [activeBackgroundShellPid, setActiveBackgroundShellPid] = useState< - number | null - >(null); - - const prevShellCountRef = useRef(backgroundTaskCount); - - useEffect(() => { - if (backgroundTasks.size === 0) { - if (activeBackgroundShellPid !== null) { - setActiveBackgroundShellPid(null); - } - if (isBackgroundShellListOpen) { - setIsBackgroundShellListOpen(false); - } - } else if ( - activeBackgroundShellPid === null || - !backgroundTasks.has(activeBackgroundShellPid) - ) { - // If active shell is closed or none selected, select the first one - setActiveBackgroundShellPid(backgroundTasks.keys().next().value ?? null); - } else if (backgroundTaskCount > prevShellCountRef.current) { - // A new shell was added — auto-switch to the newest one (last in the map) - const pids = Array.from(backgroundTasks.keys()); - const newestPid = pids[pids.length - 1]; - if (newestPid !== undefined && newestPid !== activeBackgroundShellPid) { - setActiveBackgroundShellPid(newestPid); - } - } - prevShellCountRef.current = backgroundTaskCount; - }, [ - backgroundTasks, - activeBackgroundShellPid, - backgroundTaskCount, - isBackgroundShellListOpen, - ]); - - useEffect(() => { - if (embeddedShellFocused) { - const hasActiveForegroundShell = !!activePtyId; - const hasVisibleBackgroundShell = - isBackgroundTaskVisible && backgroundTasks.size > 0; - - if (!hasActiveForegroundShell && !hasVisibleBackgroundShell) { - setEmbeddedShellFocused(false); - } - } - }, [ - isBackgroundTaskVisible, - backgroundTasks, - embeddedShellFocused, - backgroundTaskCount, - activePtyId, - setEmbeddedShellFocused, - ]); - - const backgroundShellHeight = useMemo( - () => - isBackgroundTaskVisible && backgroundTasks.size > 0 - ? Math.max(Math.floor(terminalHeight * 0.3), 5) - : 0, - [isBackgroundTaskVisible, backgroundTasks.size, terminalHeight], - ); - - return { - isBackgroundShellListOpen, - setIsBackgroundShellListOpen, - activeBackgroundShellPid, - setActiveBackgroundShellPid, - backgroundShellHeight, - }; -} diff --git a/packages/cli/src/ui/hooks/useExecutionLifecycle.ts b/packages/cli/src/ui/hooks/useExecutionLifecycle.ts index 02e9e88cf5..2e80bf8f95 100644 --- a/packages/cli/src/ui/hooks/useExecutionLifecycle.ts +++ b/packages/cli/src/ui/hooks/useExecutionLifecycle.ts @@ -661,10 +661,6 @@ export const useExecutionLifecycle = ( (s: BackgroundTask) => s.status === 'running', ).length; - const showBackgroundShell = useCallback(() => { - dispatch({ type: 'SET_VISIBILITY', visible: true }); - }, [dispatch]); - return { handleShellCommand, activeShellPtyId: state.activeShellPtyId, @@ -672,7 +668,6 @@ export const useExecutionLifecycle = ( backgroundTaskCount, isBackgroundTaskVisible: state.isBackgroundTaskVisible, toggleBackgroundTasks, - showBackgroundShell, backgroundCurrentExecution, registerBackgroundTask, dismissBackgroundTask, diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index c4a9c58d5e..a2621c4546 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -390,7 +390,6 @@ export const useGeminiStream = ( backgroundTaskCount, isBackgroundTaskVisible, toggleBackgroundTasks, - showBackgroundShell, backgroundCurrentExecution, registerBackgroundTask, dismissBackgroundTask, @@ -1918,7 +1917,6 @@ export const useGeminiStream = ( backgroundedTool.command, backgroundedTool.initialOutput, ); - showBackgroundShell(); } } @@ -2058,7 +2056,6 @@ export const useGeminiStream = ( modelSwitchedFromQuotaError, addItem, registerBackgroundTask, - showBackgroundShell, consumeUserHint, isLowErrorVerbosity, maybeAddSuppressedToolErrorNote, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index c82cc315b7..0edd4af7b0 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -36,8 +36,6 @@ import { GlobTool } from '../tools/glob.js'; import { ActivateSkillTool } from '../tools/activate-skill.js'; import { EditTool } from '../tools/edit.js'; import { ShellTool } from '../tools/shell.js'; -import { WriteToShellTool } from '../tools/write-to-shell.js'; -import { ReadShellTool } from '../tools/read-shell.js'; import { WriteFileTool } from '../tools/write-file.js'; import { WebFetchTool } from '../tools/web-fetch.js'; import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js'; @@ -658,7 +656,6 @@ export interface ConfigParameters { useRipgrep?: boolean; enableInteractiveShell?: boolean; shellBackgroundCompletionBehavior?: string; - interactiveShellMode?: 'human' | 'ai' | 'off'; skipNextSpeakerCheck?: boolean; shellExecutionConfig?: ShellExecutionConfig; extensionManagement?: boolean; @@ -871,7 +868,6 @@ export class Config implements McpContext, AgentLoopContext { | 'inject' | 'notify' | 'silent'; - private readonly interactiveShellMode: 'human' | 'ai' | 'off'; private readonly skipNextSpeakerCheck: boolean; private readonly useBackgroundColor: boolean; private readonly useAlternateBuffer: boolean; @@ -1239,14 +1235,6 @@ export class Config implements McpContext, AgentLoopContext { this.shellBackgroundCompletionBehavior = 'silent'; } - // interactiveShellMode takes precedence over enableInteractiveShell. - // If not set, derive from enableInteractiveShell for backward compat. - if (params.interactiveShellMode) { - this.interactiveShellMode = params.interactiveShellMode; - } else { - this.interactiveShellMode = this.enableInteractiveShell ? 'human' : 'off'; - } - this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true; this.shellExecutionConfig = { terminalWidth: params.shellExecutionConfig?.terminalWidth ?? 80, @@ -3223,14 +3211,10 @@ export class Config implements McpContext, AgentLoopContext { return ( this.interactive && this.ptyInfo !== 'child_process' && - this.interactiveShellMode !== 'off' + this.enableInteractiveShell ); } - getInteractiveShellMode(): 'human' | 'ai' | 'off' { - return this.interactiveShellMode; - } - isSkillsSupportEnabled(): boolean { return this.skillsSupport; } @@ -3591,15 +3575,6 @@ export class Config implements McpContext, AgentLoopContext { new ReadBackgroundOutputTool(this, this.messageBus), ), ); - // Register AI-driven interactive shell tools when mode is 'ai' - if (this.getInteractiveShellMode() === 'ai') { - maybeRegister(WriteToShellTool, () => - registry.registerTool(new WriteToShellTool(this.messageBus)), - ); - maybeRegister(ReadShellTool, () => - registry.registerTool(new ReadShellTool(this.messageBus)), - ); - } if (!this.isMemoryManagerEnabled()) { maybeRegister(MemoryTool, () => registry.registerTool(new MemoryTool(this.messageBus, this.storage)), diff --git a/packages/core/src/prompts/promptProvider.ts b/packages/core/src/prompts/promptProvider.ts index c4077afc95..0036dae560 100644 --- a/packages/core/src/prompts/promptProvider.ts +++ b/packages/core/src/prompts/promptProvider.ts @@ -200,7 +200,6 @@ export class PromptProvider { enableShellEfficiency: context.config.getEnableShellOutputEfficiency(), interactiveShellEnabled: context.config.isInteractiveShellEnabled(), - interactiveShellMode: context.config.getInteractiveShellMode(), topicUpdateNarration: context.config.isTopicUpdateNarrationEnabled(), memoryManagerEnabled: context.config.isMemoryManagerEnabled(), diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index b049ddf58e..59315e1ca6 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -18,8 +18,6 @@ import { MEMORY_TOOL_NAME, READ_FILE_TOOL_NAME, SHELL_TOOL_NAME, - WRITE_TO_SHELL_TOOL_NAME, - READ_SHELL_TOOL_NAME, WRITE_FILE_TOOL_NAME, WRITE_TODOS_TOOL_NAME, GREP_PARAM_TOTAL_MAX_MATCHES, @@ -83,7 +81,6 @@ export interface PrimaryWorkflowsOptions { export interface OperationalGuidelinesOptions { interactive: boolean; interactiveShellEnabled: boolean; - interactiveShellMode?: 'human' | 'ai' | 'off'; topicUpdateNarration: boolean; memoryManagerEnabled: boolean; } @@ -394,7 +391,7 @@ export function renderOperationalGuidelines( - **Command Execution:** Use the ${formatToolName(SHELL_TOOL_NAME)} tool for running shell commands, remembering the safety rule to explain modifying commands first.${toolUsageInteractive( options.interactive, options.interactiveShellEnabled, - )}${toolUsageRememberingFacts(options)}${toolUsageAiShell(options)} + )}${toolUsageRememberingFacts(options)} - **Confirmation Protocol:** If a tool call is declined or cancelled, respect the decision immediately. Do not re-attempt the action or "negotiate" for the same tool call unless the user explicitly directs you to. Offer an alternative technical path if possible. ## Interaction Details @@ -803,17 +800,6 @@ function toolUsageInteractive( - **Interactive Commands:** Always prefer non-interactive commands (e.g., using 'run once' or 'CI' flags for test runners to avoid persistent watch modes or 'git --no-pager') unless a persistent process is specifically required; however, some commands are only interactive and expect user input during their execution (e.g. ssh, vim).`; } -function toolUsageAiShell(options: OperationalGuidelinesOptions): string { - if (options.interactiveShellMode !== 'ai') return ''; - return ` -- **AI-Driven Interactive Shell:** Commands using \`wait_for_output_seconds\` auto-promote to background when they stall. Once promoted, use ${formatToolName(READ_SHELL_TOOL_NAME)} to see the terminal screen, then ${formatToolName(WRITE_TO_SHELL_TOOL_NAME)} to send text input and/or special keys (arrows, Enter, Ctrl-C, etc.). - - Set \`wait_for_output_seconds\` **low (2-5)** for commands that prompt for input (npx, installers, REPLs). Set **high (60+)** for long builds. Omit for instant commands. - - **Always read the screen before writing input.** The screen state tells you what the process is waiting for. - - When waiting for a command to finish (e.g. npm install), use ${formatToolName(READ_SHELL_TOOL_NAME)} with \`wait_seconds\` to delay before reading. Do NOT poll in a tight loop. - - **Clean up when done:** when your task is complete, kill background processes with ${formatToolName(WRITE_TO_SHELL_TOOL_NAME)} sending Ctrl-C, or note the PID for the user to clean up. - - You are the sole operator of promoted shells — the user cannot type into them.`; -} - function toolUsageRememberingFacts( options: OperationalGuidelinesOptions, ): string { diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index 95b3f2d17b..dfbb3a5033 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -105,7 +105,6 @@ export interface ShellExecutionConfig { backgroundCompletionBehavior?: 'inject' | 'notify' | 'silent'; originalCommand?: string; sessionId?: string; - autoPromoteTimeoutMs?: number; } /** @@ -890,21 +889,6 @@ export class ShellExecutionService { sessionId: shellExecutionConfig.sessionId, }); - let autoPromoteTimer: NodeJS.Timeout | undefined; - const resetAutoPromoteTimer = () => { - if (shellExecutionConfig.autoPromoteTimeoutMs !== undefined) { - if (autoPromoteTimer) clearTimeout(autoPromoteTimer); - autoPromoteTimer = setTimeout(() => { - ShellExecutionService.background( - ptyPid, - shellExecutionConfig.sessionId, - ); - }, shellExecutionConfig.autoPromoteTimeoutMs); - } - }; - - resetAutoPromoteTimer(); - const result = ExecutionLifecycleService.attachExecution(ptyPid, { executionMethod: ptyInfo?.name ?? 'node-pty', writeInput: (input) => { @@ -1082,7 +1066,6 @@ export class ShellExecutionService { }); const handleOutput = (data: Buffer) => { - resetAutoPromoteTimer(); processingChain = processingChain.then( () => new Promise((resolveChunk) => { @@ -1152,7 +1135,6 @@ export class ShellExecutionService { ptyProcess.onExit( ({ exitCode, signal }: { exitCode: number; signal?: number }) => { - if (autoPromoteTimer) clearTimeout(autoPromoteTimer); exited = true; abortSignal.removeEventListener('abort', abortHandler); // Attempt to destroy the PTY to ensure FD is closed @@ -1238,7 +1220,6 @@ export class ShellExecutionService { ); const abortHandler = async () => { - if (autoPromoteTimer) clearTimeout(autoPromoteTimer); if (ptyProcess.pid && !exited) { await killProcessGroup({ pid: ptyPid, @@ -1417,28 +1398,6 @@ export class ShellExecutionService { return ExecutionLifecycleService.subscribe(pid, listener); } - /** - * Reads the current rendered screen state of a running process. - * Returns the full terminal buffer text for PTY processes, - * or the accumulated output for child processes. - * - * @param pid The process ID of the target process. - * @returns The screen text, or null if the process is not found. - */ - static readScreen(pid: number): string | null { - const activePty = this.activePtys.get(pid); - if (activePty) { - return getFullBufferText(activePty.headlessTerminal); - } - - const activeChild = this.activeChildProcesses.get(pid); - if (activeChild) { - return activeChild.state.output; - } - - return null; - } - /** * Resizes the pseudo-terminal (PTY) of a running process. * diff --git a/packages/core/src/tools/definitions/base-declarations.ts b/packages/core/src/tools/definitions/base-declarations.ts index e1575966af..89a5aa1614 100644 --- a/packages/core/src/tools/definitions/base-declarations.ts +++ b/packages/core/src/tools/definitions/base-declarations.ts @@ -56,18 +56,6 @@ export const READ_FILE_PARAM_END_LINE = 'end_line'; export const SHELL_TOOL_NAME = 'run_shell_command'; export const SHELL_PARAM_COMMAND = 'command'; export const SHELL_PARAM_IS_BACKGROUND = 'is_background'; -export const SHELL_PARAM_WAIT_SECONDS = 'wait_for_output_seconds'; - -// -- write_to_shell -- -export const WRITE_TO_SHELL_TOOL_NAME = 'write_to_shell'; -export const WRITE_TO_SHELL_PARAM_PID = 'pid'; -export const WRITE_TO_SHELL_PARAM_INPUT = 'input'; -export const WRITE_TO_SHELL_PARAM_SPECIAL_KEYS = 'special_keys'; - -// -- read_shell -- -export const READ_SHELL_TOOL_NAME = 'read_shell'; -export const READ_SHELL_PARAM_PID = 'pid'; -export const READ_SHELL_PARAM_WAIT_SECONDS = 'wait_seconds'; // -- write_file -- export const WRITE_FILE_TOOL_NAME = 'write_file'; diff --git a/packages/core/src/tools/definitions/coreTools.ts b/packages/core/src/tools/definitions/coreTools.ts index a70ed1a33c..d1b81a6e99 100644 --- a/packages/core/src/tools/definitions/coreTools.ts +++ b/packages/core/src/tools/definitions/coreTools.ts @@ -27,8 +27,6 @@ export { LS_TOOL_NAME, READ_FILE_TOOL_NAME, SHELL_TOOL_NAME, - WRITE_TO_SHELL_TOOL_NAME, - READ_SHELL_TOOL_NAME, WRITE_FILE_TOOL_NAME, EDIT_TOOL_NAME, WEB_SEARCH_TOOL_NAME, @@ -75,12 +73,6 @@ export { LS_PARAM_IGNORE, SHELL_PARAM_COMMAND, SHELL_PARAM_IS_BACKGROUND, - SHELL_PARAM_WAIT_SECONDS, - WRITE_TO_SHELL_PARAM_PID, - WRITE_TO_SHELL_PARAM_INPUT, - WRITE_TO_SHELL_PARAM_SPECIAL_KEYS, - READ_SHELL_PARAM_PID, - READ_SHELL_PARAM_WAIT_SECONDS, WEB_SEARCH_PARAM_QUERY, WEB_FETCH_PARAM_PROMPT, READ_MANY_PARAM_INCLUDE, @@ -257,21 +249,18 @@ export function getShellDefinition( enableInteractiveShell: boolean, enableEfficiency: boolean, enableToolSandboxing: boolean = false, - interactiveShellMode?: string, ): ToolDefinition { return { base: getShellDeclaration( enableInteractiveShell, enableEfficiency, enableToolSandboxing, - interactiveShellMode, ), overrides: (modelId) => getToolSet(modelId).run_shell_command( enableInteractiveShell, enableEfficiency, enableToolSandboxing, - interactiveShellMode, ), }; } diff --git a/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts b/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts index 6f001c7459..29da313bf4 100644 --- a/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts +++ b/packages/core/src/tools/definitions/dynamic-declaration-helpers.ts @@ -22,7 +22,6 @@ import { PARAM_DIR_PATH, SHELL_PARAM_IS_BACKGROUND, EXIT_PLAN_PARAM_PLAN_FILENAME, - SHELL_PARAM_WAIT_SECONDS, SKILL_PARAM_NAME, PARAM_ADDITIONAL_PERMISSIONS, UPDATE_TOPIC_TOOL_NAME, @@ -37,9 +36,7 @@ import { export function getShellToolDescription( enableInteractiveShell: boolean, enableEfficiency: boolean, - interactiveShellMode?: string, ): string { - const isAiMode = interactiveShellMode === 'ai'; const efficiencyGuidelines = enableEfficiency ? ` @@ -59,11 +56,6 @@ export function getShellToolDescription( Background PIDs: Only included if background processes were started. Process Group PGID: Only included if available.`; - if (isAiMode) { - const autoPromoteInstructions = `Commands that do not complete within \`${SHELL_PARAM_WAIT_SECONDS}\` seconds are automatically promoted to background. Once promoted, use \`write_to_shell\` and \`read_shell\` to interact with the process. Do NOT use \`&\` to background commands.`; - return `This tool executes a given shell command as \`bash -c \`. ${autoPromoteInstructions} Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \`kill -- -PGID\` or signaled as \`kill -s SIGNAL -- -PGID\`.${efficiencyGuidelines}${returnedInfo}`; - } - if (os.platform() === 'win32') { const backgroundInstructions = enableInteractiveShell ? `To run a command in the background, set the \`${SHELL_PARAM_IS_BACKGROUND}\` parameter to true. Do NOT use PowerShell background constructs.` @@ -94,33 +86,12 @@ export function getShellDeclaration( enableInteractiveShell: boolean, enableEfficiency: boolean, enableToolSandboxing: boolean = false, - interactiveShellMode?: string, ): FunctionDeclaration { - const isAiMode = interactiveShellMode === 'ai'; - - // In AI mode, use wait_for_output_seconds instead of is_background - const backgroundParam = isAiMode - ? { - [SHELL_PARAM_WAIT_SECONDS]: { - type: 'number' as const, - description: - 'Max seconds to wait for command to complete before auto-promoting to background (default: 5). Set low (2-5) for commands likely to prompt for input (npx, installers, REPLs). Set high (60-300) for long builds or installs. Once promoted, use write_to_shell/read_shell to interact.', - }, - } - : { - [SHELL_PARAM_IS_BACKGROUND]: { - type: 'boolean' as const, - description: - 'Set to true if this command should be run in the background (e.g. for long-running servers or watchers). The command will be started, allowed to run for a brief moment to check for immediate errors, and then moved to the background.', - }, - }; - return { name: SHELL_TOOL_NAME, description: getShellToolDescription( enableInteractiveShell, enableEfficiency, - interactiveShellMode, ), parametersJsonSchema: { type: 'object', @@ -149,7 +120,6 @@ export function getShellDeclaration( description: 'Optional. Delay in milliseconds to wait after starting the process in the background. Useful to allow the process to start and generate initial output before returning.', }, - ...backgroundParam, ...(enableToolSandboxing ? { [PARAM_ADDITIONAL_PERMISSIONS]: { diff --git a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts index 5441c39d09..60a52fc6ad 100644 --- a/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts +++ b/packages/core/src/tools/definitions/model-family-sets/default-legacy.ts @@ -337,13 +337,11 @@ export const DEFAULT_LEGACY_SET: CoreToolSet = { enableInteractiveShell, enableEfficiency, enableToolSandboxing, - interactiveShellMode, ) => getShellDeclaration( enableInteractiveShell, enableEfficiency, enableToolSandboxing, - interactiveShellMode, ), replace: { diff --git a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts index f29f9e6814..a86a20378e 100644 --- a/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts +++ b/packages/core/src/tools/definitions/model-family-sets/gemini-3.ts @@ -344,13 +344,11 @@ export const GEMINI_3_SET: CoreToolSet = { enableInteractiveShell, enableEfficiency, enableToolSandboxing, - interactiveShellMode, ) => getShellDeclaration( enableInteractiveShell, enableEfficiency, enableToolSandboxing, - interactiveShellMode, ), replace: { diff --git a/packages/core/src/tools/definitions/types.ts b/packages/core/src/tools/definitions/types.ts index d4f532f513..42c0cc7028 100644 --- a/packages/core/src/tools/definitions/types.ts +++ b/packages/core/src/tools/definitions/types.ts @@ -38,7 +38,6 @@ export interface CoreToolSet { enableInteractiveShell: boolean, enableEfficiency: boolean, enableToolSandboxing: boolean, - interactiveShellMode?: string, ) => FunctionDeclaration; replace: FunctionDeclaration; google_web_search: FunctionDeclaration; diff --git a/packages/core/src/tools/read-shell.ts b/packages/core/src/tools/read-shell.ts deleted file mode 100644 index 4e74cbbfa5..0000000000 --- a/packages/core/src/tools/read-shell.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * @license - * Copyright 2026 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - BaseDeclarativeTool, - BaseToolInvocation, - Kind, - type ToolInvocation, - type ToolResult, -} from './tools.js'; -import { ShellExecutionService } from '../services/shellExecutionService.js'; -import { - READ_SHELL_TOOL_NAME, - READ_SHELL_PARAM_PID, - READ_SHELL_PARAM_WAIT_SECONDS, -} from './tool-names.js'; -import type { MessageBus } from '../confirmation-bus/message-bus.js'; - -export interface ReadShellParams { - pid: number; - wait_seconds?: number; -} - -export class ReadShellToolInvocation extends BaseToolInvocation< - ReadShellParams, - ToolResult -> { - constructor( - params: ReadShellParams, - messageBus: MessageBus, - _toolName?: string, - _toolDisplayName?: string, - ) { - super(params, messageBus, _toolName, _toolDisplayName); - } - - getDescription(): string { - const waitPart = - this.params.wait_seconds !== undefined - ? ` (after ${this.params.wait_seconds}s)` - : ''; - return `read shell screen PID ${this.params.pid}${waitPart}`; - } - - async execute(signal: AbortSignal): Promise { - const { pid, wait_seconds } = this.params; - - // Wait before reading if requested - if (wait_seconds !== undefined && wait_seconds > 0) { - const waitMs = Math.min(wait_seconds, 30) * 1000; // Cap at 30s - await new Promise((resolve) => { - const timer = setTimeout(resolve, waitMs); - const onAbort = () => { - clearTimeout(timer); - resolve(); - }; - signal.addEventListener('abort', onAbort, { once: true }); - }); - } - - // Validate the PID is active - if (!ShellExecutionService.isPtyActive(pid)) { - return { - llmContent: `Error: No active process found with PID ${pid}. The process may have exited.`, - returnDisplay: `No active process with PID ${pid}.`, - }; - } - - const screen = ShellExecutionService.readScreen(pid); - if (screen === null) { - return { - llmContent: `Error: Could not read screen for PID ${pid}. The process may have exited.`, - returnDisplay: `Could not read screen for PID ${pid}.`, - }; - } - - return { - llmContent: screen, - returnDisplay: `Screen read from PID ${pid} (${screen.split('\n').length} lines).`, - }; - } -} - -export class ReadShellTool extends BaseDeclarativeTool< - ReadShellParams, - ToolResult -> { - static readonly Name = READ_SHELL_TOOL_NAME; - - constructor(messageBus: MessageBus) { - super( - ReadShellTool.Name, - 'ReadShell', - 'Reads the current screen state of a running background shell process. Returns the rendered terminal screen as text, preserving the visual layout. Use after write_to_shell to see updated output, or to check progress of a running command.', - Kind.Read, - { - type: 'object', - properties: { - [READ_SHELL_PARAM_PID]: { - type: 'number', - description: - 'The PID of the background process to read from. Obtained from a previous run_shell_command call that was auto-promoted to background or started with is_background=true.', - }, - [READ_SHELL_PARAM_WAIT_SECONDS]: { - type: 'number', - description: - 'Seconds to wait before reading the screen. Use this to let the process run for a while before checking output (e.g. wait for a build to finish). Max 30 seconds.', - }, - }, - required: [READ_SHELL_PARAM_PID], - }, - messageBus, - false, // output is not markdown - ); - } - - protected override validateToolParamValues( - params: ReadShellParams, - ): string | null { - if (!params.pid || params.pid <= 0) { - return 'PID must be a positive number.'; - } - if ( - params.wait_seconds !== undefined && - (params.wait_seconds < 0 || params.wait_seconds > 30) - ) { - return 'wait_seconds must be between 0 and 30.'; - } - return null; - } - - protected createInvocation( - params: ReadShellParams, - messageBus: MessageBus, - _toolName?: string, - _toolDisplayName?: string, - ): ToolInvocation { - return new ReadShellToolInvocation( - params, - messageBus, - _toolName, - _toolDisplayName, - ); - } -} diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 8ed78ba464..9551fd9638 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -149,8 +149,6 @@ describe('ShellTool', () => { getShellBackgroundCompletionBehavior: vi.fn().mockReturnValue('silent'), getEnableShellOutputEfficiency: vi.fn().mockReturnValue(true), getSandboxEnabled: vi.fn().mockReturnValue(false), - getInteractiveShellMode: vi.fn().mockReturnValue('off'), - getSessionId: vi.fn().mockReturnValue('test-session-id'), sanitizationConfig: {}, get sandboxManager() { return mockSandboxManager; @@ -424,7 +422,7 @@ describe('ShellTool', () => { expect(mockShellBackground).toHaveBeenCalledWith( 12345, - 'test-session-id', + 'default', 'sleep 10', ); @@ -668,7 +666,7 @@ describe('ShellTool', () => { expect(mockShellBackground).toHaveBeenCalledWith( 12345, - 'test-session-id', + 'default', 'sleep 10', ); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 0407cb99bf..3ea29474c6 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -33,7 +33,6 @@ import { import { getErrorMessage } from '../utils/errors.js'; import { summarizeToolOutput } from '../utils/summarizer.js'; -import { formatShellOutput } from './shellOutputFormatter.js'; import { ShellExecutionService, type ShellOutputEvent, @@ -72,7 +71,6 @@ export interface ShellToolParams { is_background?: boolean; delay_ms?: number; [PARAM_ADDITIONAL_PERMISSIONS]?: SandboxPermissions; - wait_for_output_seconds?: number; } export class ShellToolInvocation extends BaseToolInvocation< @@ -80,7 +78,6 @@ export class ShellToolInvocation extends BaseToolInvocation< ToolResult > { private proactivePermissionsConfirmed?: SandboxPermissions; - private _autoPromoteTimer?: NodeJS.Timeout; constructor( private readonly context: AgentLoopContext, @@ -226,12 +223,7 @@ export class ShellToolInvocation extends BaseToolInvocation< } override getExplanation(): string { - let explanation = this.getContextualDetails().trim(); - const isAiMode = this.context.config.getInteractiveShellMode() === 'ai'; - if (this.params.wait_for_output_seconds !== undefined || isAiMode) { - explanation += ` [auto-background after ${this.params.wait_for_output_seconds ?? 5}s]`; - } - return explanation; + return this.getContextualDetails().trim(); } override getPolicyUpdateOptions( @@ -505,21 +497,6 @@ export class ShellToolInvocation extends BaseToolInvocation< }, timeoutMs); }; - let currentPid: number | undefined; - const isAiMode = this.context.config.getInteractiveShellMode() === 'ai'; - const shouldAutoPromote = - this.params.wait_for_output_seconds !== undefined || isAiMode; - const waitMs = (this.params.wait_for_output_seconds ?? 5) * 1000; - - const resetAutoPromoteTimer = () => { - if (shouldAutoPromote && currentPid) { - if (this._autoPromoteTimer) clearTimeout(this._autoPromoteTimer); - this._autoPromoteTimer = setTimeout(() => { - ShellExecutionService.background(currentPid!); - }, waitMs); - } - }; - signal.addEventListener('abort', onAbort, { once: true }); timeoutController.signal.addEventListener('abort', onAbort, { once: true, @@ -534,7 +511,6 @@ export class ShellToolInvocation extends BaseToolInvocation< cwd, (event: ShellOutputEvent) => { resetTimeout(); // Reset timeout on any event - resetAutoPromoteTimer(); // Reset auto-promote on any event if (!updateOutput) { return; } @@ -606,7 +582,6 @@ export class ShellToolInvocation extends BaseToolInvocation< backgroundCompletionBehavior: this.context.config.getShellBackgroundCompletionBehavior(), originalCommand: strippedCommand, - autoPromoteTimeoutMs: shouldAutoPromote ? waitMs : undefined, }, ); @@ -643,11 +618,6 @@ export class ShellToolInvocation extends BaseToolInvocation< }; } } - - // In AI mode with wait_for_output_seconds, set up auto-promotion timer. - // When the timer fires, promote to background instead of cancelling. - currentPid = pid; - resetAutoPromoteTimer(); } const result = await resultPromise; @@ -688,73 +658,95 @@ export class ShellToolInvocation extends BaseToolInvocation< } } + let data: BackgroundExecutionData | undefined; + + let llmContent = ''; let timeoutMessage = ''; if (result.aborted) { if (timeoutController.signal.aborted) { timeoutMessage = `Command was automatically cancelled because it exceeded the timeout of ${( timeoutMs / 60000 ).toFixed(1)} minutes without output.`; + llmContent = timeoutMessage; + } else { + llmContent = + 'Command was cancelled by user before it could complete.'; } - } - - const formatterOutput = formatShellOutput({ - params: this.params, - result, - debugMode: this.context.config.getDebugMode(), - backgroundPIDs, - isAiMode, - timeoutMessage, - }); - - let data: BackgroundExecutionData | undefined; - data = formatterOutput.data as BackgroundExecutionData | undefined; - let returnDisplay: string | AnsiOutput = formatterOutput.returnDisplay; - let llmContent = formatterOutput.llmContent; - - if (!this.context.config.getDebugMode()) { - if ( - !this.params.is_background && - !result.backgrounded && - !result.aborted - ) { - if (result.output.trim() || result.ansiOutput) { - returnDisplay = - result.ansiOutput && result.ansiOutput.length > 0 - ? result.ansiOutput - : result.output; - } else { - if (result.signal) { - returnDisplay = `Command terminated by signal: ${result.signal}`; - } else if (result.error) { - returnDisplay = `Command failed: ${getErrorMessage(result.error)}`; - } else if (result.exitCode !== null && result.exitCode !== 0) { - returnDisplay = `Command exited with code: ${result.exitCode}`; - } - } + if (result.output.trim()) { + llmContent += ` Below is the output before it was cancelled:\n${result.output}`; + } else { + llmContent += ' There was no output before it was cancelled.'; } - } - - // Replace wrapper command with actual command in error messages - if (result.error && !result.aborted) { - llmContent = llmContent.replaceAll( - commandToExecute, - this.params.command, - ); - } - - // Update data with specific things needed by ShellTool - if (this.params.is_background || result.backgrounded) { + } else if (this.params.is_background || result.backgrounded) { + llmContent = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`; data = { - ...data, - initialOutput: result.output, - pid: result.pid!, + pid: result.pid, command: this.params.command, + initialOutput: result.output, }; - } else if (result.exitCode !== null && result.exitCode !== 0) { - data = { - exitCode: result.exitCode, - isError: true, - } as BackgroundExecutionData; + } else { + // Create a formatted error string for display, replacing the wrapper command + // with the user-facing command. + const llmContentParts = [`Output: ${result.output || '(empty)'}`]; + + if (result.error) { + const finalError = result.error.message.replaceAll( + commandToExecute, + this.params.command, + ); + llmContentParts.push(`Error: ${finalError}`); + } + + if (result.exitCode !== null && result.exitCode !== 0) { + llmContentParts.push(`Exit Code: ${result.exitCode}`); + data = { + exitCode: result.exitCode, + isError: true, + }; + } + + if (result.signal) { + llmContentParts.push(`Signal: ${result.signal}`); + } + if (backgroundPIDs.length) { + llmContentParts.push(`Background PIDs: ${backgroundPIDs.join(', ')}`); + } + if (result.pid) { + llmContentParts.push(`Process Group PGID: ${result.pid}`); + } + + llmContent = llmContentParts.join('\n'); + } + + let returnDisplay: string | AnsiOutput = ''; + if (this.context.config.getDebugMode()) { + returnDisplay = llmContent; + } else { + if (this.params.is_background || result.backgrounded) { + returnDisplay = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`; + } else if (result.aborted) { + const cancelMsg = timeoutMessage || 'Command cancelled by user.'; + if (result.output.trim()) { + returnDisplay = `${cancelMsg}\n\nOutput before cancellation:\n${result.output}`; + } else { + returnDisplay = cancelMsg; + } + } else if (result.output.trim() || result.ansiOutput) { + returnDisplay = + result.ansiOutput && result.ansiOutput.length > 0 + ? result.ansiOutput + : result.output; + } else { + if (result.signal) { + returnDisplay = `Command terminated by signal: ${result.signal}`; + } else if (result.error) { + returnDisplay = `Command failed: ${getErrorMessage(result.error)}`; + } else if (result.exitCode !== null && result.exitCode !== 0) { + returnDisplay = `Command exited with code: ${result.exitCode}`; + } + // If output is empty and command succeeded (code 0, no error/signal/abort), + // returnDisplay will remain empty, which is fine. + } } // Heuristic Sandbox Denial Detection @@ -937,8 +929,6 @@ export class ShellToolInvocation extends BaseToolInvocation< }; } finally { if (timeoutTimer) clearTimeout(timeoutTimer); - const autoTimer = this._autoPromoteTimer; - if (autoTimer) clearTimeout(autoTimer); signal.removeEventListener('abort', onAbort); timeoutController.signal.removeEventListener('abort', onAbort); try { @@ -1017,7 +1007,6 @@ export class ShellTool extends BaseDeclarativeTool< this.context.config.getEnableInteractiveShell(), this.context.config.getEnableShellOutputEfficiency(), this.context.config.getSandboxEnabled(), - this.context.config.getInteractiveShellMode(), ); return resolveToolDeclaration(definition, modelId); } diff --git a/packages/core/src/tools/shellOutputFormatter.ts b/packages/core/src/tools/shellOutputFormatter.ts deleted file mode 100644 index 04d16fb42e..0000000000 --- a/packages/core/src/tools/shellOutputFormatter.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * @license - * Copyright 2026 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { type ShellExecutionResult } from '../services/shellExecutionService.js'; -import { type ShellToolParams } from './shell.js'; - -export interface FormatShellOutputOptions { - params: ShellToolParams; - result: ShellExecutionResult; - debugMode: boolean; - timeoutMessage?: string; - backgroundPIDs: number[]; - summarizedOutput?: string; - isAiMode: boolean; -} - -export interface FormattedShellOutput { - llmContent: string; - returnDisplay: string; - data: Record; -} - -export function formatShellOutput( - options: FormatShellOutputOptions, -): FormattedShellOutput { - const { - params, - result, - debugMode, - timeoutMessage, - backgroundPIDs, - summarizedOutput, - } = options; - - let llmContent = ''; - let data: Record = {}; - - if (result.aborted) { - llmContent = timeoutMessage || 'Command cancelled by user.'; - if (result.output.trim()) { - llmContent += ` Below is the output before it was cancelled:\n${result.output}`; - } else { - llmContent += ' There was no output before it was cancelled.'; - } - } else if (params.is_background || result.backgrounded) { - const isAutoPromoted = result.backgrounded && !params.is_background; - if (isAutoPromoted) { - llmContent = `Command auto-promoted to background (PID: ${result.pid}). The process is still running. To check its screen state, call the read_shell tool with pid ${result.pid}. To send input or keystrokes, call the write_to_shell tool with pid ${result.pid}. If the process does not exit on its own when done, kill it with write_to_shell using special_keys=["Ctrl-C"].`; - } else { - llmContent = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`; - } - data = { - pid: result.pid, - command: params.command, - directory: params.dir_path, - backgrounded: true, - }; - } else { - const llmContentParts: string[] = []; - - let content = summarizedOutput ?? result.output.trim(); - if (!content) { - content = '(empty)'; - } - - llmContentParts.push(`Output: ${content}`); - - if (result.error) { - llmContentParts.push(`Error: ${result.error.message}`); - } - - if (result.exitCode !== null && result.exitCode !== 0) { - llmContentParts.push(`Exit Code: ${result.exitCode}`); - } - if (result.signal !== null) { - llmContentParts.push(`Signal: ${result.signal}`); - } - if (backgroundPIDs.length) { - llmContentParts.push(`Background PIDs: ${backgroundPIDs.join(', ')}`); - } - if (result.pid) { - llmContentParts.push(`Process Group PGID: ${result.pid}`); - } - - llmContent = llmContentParts.join('\n'); - } - - let returnDisplay = ''; - if (debugMode) { - returnDisplay = llmContent; - } else { - if (params.is_background || result.backgrounded) { - const isAutoPromotedDisplay = - result.backgrounded && !params.is_background; - if (isAutoPromotedDisplay) { - returnDisplay = `Command auto-promoted to background (PID: ${result.pid}).`; - } else { - returnDisplay = `Command moved to background (PID: ${result.pid}). Output hidden. Press Ctrl+B to view.`; - } - } else if (result.aborted) { - const cancelMsg = timeoutMessage || 'Command cancelled by user.'; - if (result.output.trim()) { - returnDisplay = `${cancelMsg}\n\nOutput before cancellation:\n${result.output}`; - } else { - returnDisplay = cancelMsg; - } - } else if (result.error) { - returnDisplay = `Command failed: ${result.error.message}`; - } else if (result.exitCode !== 0 && result.exitCode !== null) { - returnDisplay = `Command exited with code ${result.exitCode}`; - if (result.output.trim()) { - returnDisplay += `\n\n${result.output}`; - } - } else if (summarizedOutput) { - returnDisplay = `Command succeeded. Output summarized:\n${summarizedOutput}`; - } else { - returnDisplay = `Command succeeded.`; - if (result.output.trim()) { - returnDisplay += `\n\n${result.output}`; - } - } - } - - return { llmContent, returnDisplay, data }; -} diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index 47cc906c27..224f2ab0d5 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -10,8 +10,6 @@ import { LS_TOOL_NAME, READ_FILE_TOOL_NAME, SHELL_TOOL_NAME, - WRITE_TO_SHELL_TOOL_NAME, - READ_SHELL_TOOL_NAME, WRITE_FILE_TOOL_NAME, EDIT_TOOL_NAME, WEB_SEARCH_TOOL_NAME, @@ -54,12 +52,6 @@ import { LS_PARAM_IGNORE, SHELL_PARAM_COMMAND, SHELL_PARAM_IS_BACKGROUND, - SHELL_PARAM_WAIT_SECONDS, - WRITE_TO_SHELL_PARAM_PID, - WRITE_TO_SHELL_PARAM_INPUT, - WRITE_TO_SHELL_PARAM_SPECIAL_KEYS, - READ_SHELL_PARAM_PID, - READ_SHELL_PARAM_WAIT_SECONDS, WEB_SEARCH_PARAM_QUERY, WEB_FETCH_PARAM_PROMPT, READ_MANY_PARAM_INCLUDE, @@ -98,8 +90,6 @@ export { LS_TOOL_NAME, READ_FILE_TOOL_NAME, SHELL_TOOL_NAME, - WRITE_TO_SHELL_TOOL_NAME, - READ_SHELL_TOOL_NAME, WRITE_FILE_TOOL_NAME, EDIT_TOOL_NAME, WEB_SEARCH_TOOL_NAME, @@ -146,12 +136,6 @@ export { LS_PARAM_IGNORE, SHELL_PARAM_COMMAND, SHELL_PARAM_IS_BACKGROUND, - SHELL_PARAM_WAIT_SECONDS, - WRITE_TO_SHELL_PARAM_PID, - WRITE_TO_SHELL_PARAM_INPUT, - WRITE_TO_SHELL_PARAM_SPECIAL_KEYS, - READ_SHELL_PARAM_PID, - READ_SHELL_PARAM_WAIT_SECONDS, WEB_SEARCH_PARAM_QUERY, WEB_FETCH_PARAM_PROMPT, READ_MANY_PARAM_INCLUDE, @@ -195,7 +179,6 @@ export const TOOLS_REQUIRING_NARROWING = new Set([ WRITE_FILE_TOOL_NAME, EDIT_TOOL_NAME, SHELL_TOOL_NAME, - WRITE_TO_SHELL_TOOL_NAME, ]); export const TRACKER_CREATE_TASK_TOOL_NAME = 'tracker_create_task'; @@ -268,8 +251,6 @@ export const ALL_BUILTIN_TOOL_NAMES = [ WEB_FETCH_TOOL_NAME, EDIT_TOOL_NAME, SHELL_TOOL_NAME, - WRITE_TO_SHELL_TOOL_NAME, - READ_SHELL_TOOL_NAME, GREP_TOOL_NAME, READ_MANY_FILES_TOOL_NAME, READ_FILE_TOOL_NAME, diff --git a/packages/core/src/tools/write-to-shell.ts b/packages/core/src/tools/write-to-shell.ts deleted file mode 100644 index 652cb31bf5..0000000000 --- a/packages/core/src/tools/write-to-shell.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * @license - * Copyright 2026 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { - type ToolConfirmationOutcome, - BaseDeclarativeTool, - BaseToolInvocation, - Kind, - type ToolInvocation, - type ToolResult, - type ToolCallConfirmationDetails, - type ToolExecuteConfirmationDetails, -} from './tools.js'; -import { ShellExecutionService } from '../services/shellExecutionService.js'; -import { - WRITE_TO_SHELL_TOOL_NAME, - WRITE_TO_SHELL_PARAM_PID, - WRITE_TO_SHELL_PARAM_INPUT, - WRITE_TO_SHELL_PARAM_SPECIAL_KEYS, -} from './tool-names.js'; -import type { MessageBus } from '../confirmation-bus/message-bus.js'; - -/** - * Mapping of named special keys to their ANSI escape sequences. - */ -const SPECIAL_KEY_MAP: Record = { - Enter: '\r', - Tab: '\t', - Up: '\x1b[A', - Down: '\x1b[B', - Left: '\x1b[D', - Right: '\x1b[C', - Escape: '\x1b', - Backspace: '\x7f', - 'Ctrl-C': '\x03', - 'Ctrl-D': '\x04', - 'Ctrl-Z': '\x1a', - Space: ' ', - Delete: '\x1b[3~', - Home: '\x1b[H', - End: '\x1b[F', -}; - -const VALID_SPECIAL_KEYS = Object.keys(SPECIAL_KEY_MAP); - -/** Delay in ms to wait after writing input for the process to react. */ -const POST_INPUT_DELAY_MS = 150; - -export interface WriteToShellParams { - pid: number; - input?: string; - special_keys?: string[]; -} - -export class WriteToShellToolInvocation extends BaseToolInvocation< - WriteToShellParams, - ToolResult -> { - constructor( - params: WriteToShellParams, - messageBus: MessageBus, - _toolName?: string, - _toolDisplayName?: string, - ) { - super(params, messageBus, _toolName, _toolDisplayName); - } - - getDescription(): string { - const parts: string[] = [`write to shell PID ${this.params.pid}`]; - if (this.params.input) { - const display = - this.params.input.length > 50 - ? `${this.params.input.substring(0, 50)}...` - : this.params.input; - parts.push(`input: "${display}"`); - } - if (this.params.special_keys?.length) { - parts.push(`keys: [${this.params.special_keys.join(', ')}]`); - } - return parts.join(' '); - } - - protected override async getConfirmationDetails( - _abortSignal: AbortSignal, - ): Promise { - const confirmationDetails: ToolExecuteConfirmationDetails = { - type: 'exec', - title: 'Confirm Shell Input', - command: this.getDescription(), - rootCommand: 'write_to_shell', - rootCommands: ['write_to_shell'], - onConfirm: async (_outcome: ToolConfirmationOutcome) => { - // Policy updates handled centrally - }, - }; - return confirmationDetails; - } - - async execute(_signal: AbortSignal): Promise { - const { pid, input, special_keys } = this.params; - - // Validate the PID is active - if (!ShellExecutionService.isPtyActive(pid)) { - return { - llmContent: `Error: No active process found with PID ${pid}. The process may have exited.`, - returnDisplay: `No active process with PID ${pid}.`, - }; - } - - // Validate special keys - if (special_keys?.length) { - const invalidKeys = special_keys.filter( - (k) => !VALID_SPECIAL_KEYS.includes(k), - ); - if (invalidKeys.length > 0) { - return { - llmContent: `Error: Invalid special keys: ${invalidKeys.join(', ')}. Valid keys are: ${VALID_SPECIAL_KEYS.join(', ')}`, - returnDisplay: `Invalid special keys: ${invalidKeys.join(', ')}`, - }; - } - } - - // Send text input - if (input) { - ShellExecutionService.writeToPty(pid, input); - } - - // Send special keys - if (special_keys?.length) { - for (const key of special_keys) { - const sequence = SPECIAL_KEY_MAP[key]; - if (sequence) { - ShellExecutionService.writeToPty(pid, sequence); - } - } - } - - // Wait briefly for the process to react - await new Promise((resolve) => setTimeout(resolve, POST_INPUT_DELAY_MS)); - - // Read the screen after writing - const screen = ShellExecutionService.readScreen(pid); - if (screen === null) { - return { - llmContent: `Input sent, but the process (PID ${pid}) has exited.`, - returnDisplay: `Process exited after input.`, - }; - } - - return { - llmContent: `Input sent to PID ${pid}. Current screen:\n${screen}`, - returnDisplay: `Input sent to PID ${pid}.`, - }; - } -} - -export class WriteToShellTool extends BaseDeclarativeTool< - WriteToShellParams, - ToolResult -> { - static readonly Name = WRITE_TO_SHELL_TOOL_NAME; - - constructor(messageBus: MessageBus) { - super( - WriteToShellTool.Name, - 'WriteToShell', - 'Sends input to a running background shell process. Use this to interact with TUI applications, REPLs, and interactive commands. After writing, the current screen state is returned. Works with processes that were auto-promoted to background via wait_for_output_seconds or started with is_background=true.', - Kind.Execute, - { - type: 'object', - properties: { - [WRITE_TO_SHELL_PARAM_PID]: { - type: 'number', - description: - 'The PID of the background process to write to. Obtained from a previous run_shell_command call that was auto-promoted to background or started with is_background=true.', - }, - [WRITE_TO_SHELL_PARAM_INPUT]: { - type: 'string', - description: - '(OPTIONAL) Text to send to the process. This is literal text typed into the terminal.', - }, - [WRITE_TO_SHELL_PARAM_SPECIAL_KEYS]: { - type: 'array', - items: { - type: 'string', - enum: VALID_SPECIAL_KEYS, - }, - description: - '(OPTIONAL) Named special keys to send after the input text. Each key is sent in sequence. Examples: ["Enter"], ["Tab"], ["Up", "Enter"], ["Ctrl-C"].', - }, - }, - required: [WRITE_TO_SHELL_PARAM_PID], - }, - messageBus, - false, // output is not markdown - ); - } - - protected override validateToolParamValues( - params: WriteToShellParams, - ): string | null { - if (!params.pid || params.pid <= 0) { - return 'PID must be a positive number.'; - } - if ( - !params.input && - (!params.special_keys || !params.special_keys.length) - ) { - return 'At least one of input or special_keys must be provided.'; - } - return null; - } - - protected createInvocation( - params: WriteToShellParams, - messageBus: MessageBus, - _toolName?: string, - _toolDisplayName?: string, - ): ToolInvocation { - return new WriteToShellToolInvocation( - params, - messageBus, - _toolName, - _toolDisplayName, - ); - } -}