Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.',
Comment thread
Tyriar marked this conversation as resolved.
},
},
required: [
'command',
'explanation',
'isBackground',
'timeout',
]
}
};
Expand All @@ -249,6 +254,7 @@ export interface IRunInTerminalInputParams {
command: string;
explanation: string;
isBackground: boolean;
timeout?: number;
}

/**
Expand Down Expand Up @@ -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<void> | 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();
}
Comment thread
Tyriar marked this conversation as resolved.
});
}
}
Comment thread
Tyriar marked this conversation as resolved.

try {
let strategy: ITerminalExecuteStrategy;
switch (toolTerminal.shellIntegrationQuality) {
Expand Down Expand Up @@ -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 ?? '';
Comment thread
Tyriar marked this conversation as resolved.
Comment thread
Tyriar marked this conversation as resolved.
} 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, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -470,6 +471,16 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
`- \`fish\`: ${localize('preventShellHistory.description.fish', "Sets `fish_private_mode` to prevent any command from entering history")}`,
`- \`pwsh\`: ${localize('preventShellHistory.description.pwsh', "Sets a custom history handler via PSReadLine's `AddToHistoryHandler` to prevent any command from entering history")}`,
].join('\n'),
},
[TerminalChatAgentToolsSettingId.EnforceTimeoutFromModel]: {
restricted: true,
type: 'boolean',
default: false,
tags: ['experimental'],
experiment: {
mode: 'auto'
},
markdownDescription: localize('enforceTimeoutFromModel.description', "Whether to enforce the timeout value provided by the model in the run in terminal tool. When enabled, if the model provides a timeout parameter, the tool will stop tracking the command after that duration and return the output collected so far."),
}
};

Expand Down
Loading