diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts index 7466e60ed20..213469a242c 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import type { IMarker as IXtermMarker } from '@xterm/xterm'; -import { timeout } from '../../../../../../base/common/async.js'; +import { timeout, type CancelablePromise } from '../../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; @@ -220,11 +220,16 @@ export async function createRunInTerminalToolData( type: 'boolean', description: 'Whether the command starts a background process. If true, the command will run in the background and you will not see the output. If false, the tool call will block on the command finishing, and then you will get the output. Examples of background processes: building in watch mode, starting a server. You can check the output of a background process later on by using get_terminal_output.' }, + timeout: { + type: 'number', + description: 'An optional timeout in milliseconds. When provided, the tool will stop tracking the command after this duration and return the output collected so far. Be conservative with the timeout duration, give enough time that the command would complete on a low-end machine. Use 0 for no timeout. If it\'s not clear how long the command will take then use 0 to avoid prematurely terminating it, never guess too low.', + }, }, required: [ 'command', 'explanation', 'isBackground', + 'timeout', ] } }; @@ -249,6 +254,7 @@ export interface IRunInTerminalInputParams { command: string; explanation: string; isBackground: boolean; + timeout?: number; } /** @@ -662,7 +668,24 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let outputLineCount = -1; let exitCode: number | undefined; let altBufferResult: IToolResult | undefined; + let didTimeout = false; + let timeoutPromise: CancelablePromise | undefined; const executeCancellation = store.add(new CancellationTokenSource(token)); + + // Set up timeout if provided and the setting is enabled + if (args.timeout !== undefined && args.timeout > 0) { + const shouldEnforceTimeout = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnforceTimeoutFromModel) === true; + if (shouldEnforceTimeout) { + timeoutPromise = timeout(args.timeout); + timeoutPromise.then(() => { + if (!executeCancellation.token.isCancellationRequested) { + didTimeout = true; + executeCancellation.cancel(); + } + }); + } + } + try { let strategy: ITerminalExecuteStrategy; switch (toolTerminal.shellIntegrationQuality) { @@ -740,11 +763,21 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { } } catch (e) { - this._logService.debug(`RunInTerminalTool: Threw exception`); - toolTerminal.instance.dispose(); - error = e instanceof CancellationError ? 'canceled' : 'unexpectedException'; - throw e; + // Handle timeout case - get output collected so far and return it + if (didTimeout && e instanceof CancellationError) { + this._logService.debug(`RunInTerminalTool: Timeout reached, returning output collected so far`); + error = 'timeout'; + const timeoutOutput = getOutput(toolTerminal.instance, undefined); + outputLineCount = timeoutOutput ? count(timeoutOutput.trim(), '\n') + 1 : 0; + terminalResult = timeoutOutput ?? ''; + } else { + this._logService.debug(`RunInTerminalTool: Threw exception`); + toolTerminal.instance.dispose(); + error = e instanceof CancellationError ? 'canceled' : 'unexpectedException'; + throw e; + } } finally { + timeoutPromise?.cancel(); store.dispose(); const timingExecuteMs = Date.now() - timingStart; this._telemetry.logInvoke(toolTerminal.instance, { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 1d8c5062a04..3ae14b39417 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -22,6 +22,7 @@ export const enum TerminalChatAgentToolsSettingId { AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts', OutputLocation = 'chat.tools.terminal.outputLocation', PreventShellHistory = 'chat.tools.terminal.preventShellHistory', + EnforceTimeoutFromModel = 'chat.tools.terminal.enforceTimeoutFromModel', TerminalProfileLinux = 'chat.tools.terminal.terminalProfile.linux', TerminalProfileMacOs = 'chat.tools.terminal.terminalProfile.osx', @@ -470,6 +471,16 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary