Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -10,6 +10,13 @@ fi

VSCODE_SHELL_INTEGRATION=1

# Configure history exclusion for space-prefixed commands when requested by VS Code
# This is used by Copilot terminals to prevent AI-executed commands from polluting history
if [ "${VSCODE_EXCLUDE_FROM_HISTORY:-}" = "1" ]; then
export HISTCONTROL="ignorespace"
fi
unset VSCODE_EXCLUDE_FROM_HISTORY

vsc_env_keys=()
vsc_env_values=()
use_associative_array=0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ fi
# as disable it by unsetting the variable.
VSCODE_SHELL_INTEGRATION=1

# Configure history exclusion for space-prefixed commands when requested by VS Code
# This is used by Copilot terminals to prevent AI-executed commands from polluting history
if [ "${VSCODE_EXCLUDE_FROM_HISTORY:-}" = "1" ]; then
setopt HIST_IGNORE_SPACE
fi
unset VSCODE_EXCLUDE_FROM_HISTORY

# By default, zsh will set the $HISTFILE to the $ZDOTDIR location automatically. In the case of the
# shell integration being injected, this means that the terminal will use a different history file
# to other terminals. To fix this issue, set $HISTFILE back to the default location before ~/.zshrc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ or exit
set --global VSCODE_SHELL_INTEGRATION 1
set --global __vscode_shell_env_reporting $VSCODE_SHELL_ENV_REPORTING
set -e VSCODE_SHELL_ENV_REPORTING

# Enable fish private mode to exclude commands from history when requested by VS Code
# This is used by Copilot terminals to prevent AI-executed commands from polluting history
if test "$VSCODE_EXCLUDE_FROM_HISTORY" = "1"
set -g fish_private_mode 1
end
set -e VSCODE_EXCLUDE_FROM_HISTORY
set -g envVarsToReport
if test -n "$__vscode_shell_env_reporting"
set envVarsToReport (string split "," "$__vscode_shell_env_reporting")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,14 @@ function Set-MappedKeyHandlers {

if ($Global:__VSCodeState.HasPSReadLine) {
Set-MappedKeyHandlers

# Configure history exclusion for space-prefixed commands when requested by VS Code
# This is used by Copilot terminals to prevent AI-executed commands from polluting history
if ($env:VSCODE_EXCLUDE_FROM_HISTORY -eq "1") {
Set-PSReadLineOption -AddToHistoryHandler {
param([string]$line)
return -not $line.StartsWith(' ')
}
}
$env:VSCODE_EXCLUDE_FROM_HISTORY = $null
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { CancellationError } from '../../../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../../../base/common/event.js';
import { DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
import { isNumber } from '../../../../../../base/common/types.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import type { ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
import { trackIdleOnPrompt, waitForIdle, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js';
import type { IMarker as IXtermMarker } from '@xterm/xterm';
import { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js';
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';

/**
* This strategy is used when shell integration is enabled, but rich command detection was not
Expand Down Expand Up @@ -49,6 +51,7 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy {
private readonly _instance: ITerminalInstance,
private readonly _hasReceivedUserInput: () => boolean,
private readonly _commandDetection: ICommandDetectionCapability,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalLogService private readonly _logService: ITerminalLogService,
) {
}
Expand Down Expand Up @@ -121,8 +124,12 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy {
// is used as it's more common to not recognize the prompt input which would result in
// ^C being sent and also to return the exit code of 130 when from the shell when that
// occurs.
// Prefix with space to exclude from shell history (requires HISTCONTROL=ignorespace
// or HIST_IGNORE_SPACE=1 env var which is set when the terminal is created)
const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true;
const commandToSend = preventShellHistory ? ` ${commandLine}` : commandLine;
this._log(`Executing command line \`${commandLine}\``);
this._instance.sendText(commandLine, true);
this._instance.sendText(commandToSend, true);

// Wait for the next end execution event - note that this may not correspond to the actual
// execution requested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import type { CancellationToken } from '../../../../../../base/common/cancellati
import { CancellationError } from '../../../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../../../base/common/event.js';
import { DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
import { waitForIdle, waitForIdleWithPromptHeuristics, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js';
import type { IMarker as IXtermMarker } from '@xterm/xterm';
import { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js';
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';

/**
* This strategy is used when no shell integration is available. There are very few extension APIs
Expand All @@ -30,6 +32,7 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy {
constructor(
private readonly _instance: ITerminalInstance,
private readonly _hasReceivedUserInput: () => boolean,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalLogService private readonly _logService: ITerminalLogService,
) {
}
Expand Down Expand Up @@ -75,8 +78,12 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy {
// IMPORTANT: This uses `sendText` not `runCommand` since when no shell integration
// is used as sending ctrl+c before a shell is initialized (eg. PSReadLine) can result
// in failure (https://github.com/microsoft/vscode/issues/258989)
// Prefix with space to exclude from shell history (requires HISTCONTROL=ignorespace
// or HIST_IGNORE_SPACE=1 env var which is set when the terminal is created)
const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true;
const commandToSend = preventShellHistory ? ` ${commandLine}` : commandLine;
this._log(`Executing command line \`${commandLine}\``);
this._instance.sendText(commandLine, true);
this._instance.sendText(commandToSend, true);

// Assume the command is done when it's idle
this._log('Waiting for idle with prompt heuristics');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { CancellationError } from '../../../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../../../base/common/event.js';
import { DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js';
import { isNumber } from '../../../../../../base/common/types.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import type { ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js';
import { trackIdleOnPrompt, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js';
import type { IMarker as IXtermMarker } from '@xterm/xterm';
import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js';
import { TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js';

/**
* This strategy is used when the terminal has rich shell integration/command detection is
Expand All @@ -32,6 +34,7 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy {
constructor(
private readonly _instance: ITerminalInstance,
private readonly _commandDetection: ICommandDetectionCapability,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalLogService private readonly _logService: ITerminalLogService,
) {
}
Expand Down Expand Up @@ -76,8 +79,12 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy {
);

// Execute the command
// Prefix with space to exclude from shell history (requires HISTCONTROL=ignorespace
// or HIST_IGNORE_SPACE=1 env var which is set when the terminal is created)
const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true;
const commandToSend = preventShellHistory ? ` ${commandLine}` : commandLine;
this._log(`Executing command line \`${commandLine}\``);
this._instance.runCommand(commandLine, true, commandId);
this._instance.runCommand(commandToSend, true, commandId);

// Wait for the terminal to idle
this._log('Waiting for done event');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Codicon } from '../../../../../base/common/codicons.js';
import { CancellationError } from '../../../../../base/common/errors.js';
import { Event } from '../../../../../base/common/event.js';
import { DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { basename } from '../../../../../base/common/path.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { hasKey, isNumber, isObject, isString } from '../../../../../base/common/types.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
Expand All @@ -17,6 +18,7 @@ import { PromptInputState } from '../../../../../platform/terminal/common/capabi
import { ITerminalLogService, ITerminalProfile, TerminalSettingId, type IShellLaunchConfig } from '../../../../../platform/terminal/common/terminal.js';
import { ITerminalService, type ITerminalInstance } from '../../../terminal/browser/terminal.js';
import { getShellIntegrationTimeout } from '../../../terminal/common/terminalEnvironment.js';
import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js';

const enum ShellLaunchType {
Unknown = 0,
Expand Down Expand Up @@ -139,14 +141,31 @@ export class ToolTerminalCreator {
}

private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile) {
const shellPath = isString(shellOrProfile) ? shellOrProfile : shellOrProfile.path;
const shellBasename = basename(shellPath).toLowerCase().replace(/\.exe$/i, '');

// Check if the shell supports history exclusion via shell integration scripts
const shellSupportsHistoryExclusion = /^(bash|zsh|pwsh|powershell|fish)$/.test(shellBasename);
const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true;

const env: Record<string, string> = {
// Avoid making `git diff` interactive when called from copilot
GIT_PAGER: 'cat',
};

// Configure shells to ignore commands prefixed with a space from history.
// This works together with the space prefix added to commands to prevent
// copilot-executed commands from polluting the user's shell history.
// VSCODE_EXCLUDE_FROM_HISTORY=1 is handled by shell integration scripts for all shells.
if (preventShellHistory && shellSupportsHistoryExclusion) {
env['VSCODE_EXCLUDE_FROM_HISTORY'] = '1';
}

const config: IShellLaunchConfig = {
icon: ThemeIcon.fromId(Codicon.chatSparkle.id),
hideFromUser: true,
forcePersist: true,
env: {
// Avoid making `git diff` interactive when called from copilot
GIT_PAGER: 'cat',
}
env,
};

if (isString(shellOrProfile)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
let pollingResult: IPollingResult & { pollDurationMs: number } | undefined;
try {
this._logService.debug(`RunInTerminalTool: Starting background execution \`${command}\``);
const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId, commandId);
const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true;
const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId, preventShellHistory, commandId);
RunInTerminalTool._backgroundExecutions.set(termId, execution);

outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, execution, undefined, invocation.context!, token, command));
Expand Down Expand Up @@ -957,12 +958,16 @@ class BackgroundTerminalExecution extends Disposable {
private readonly _xterm: XtermTerminal,
private readonly _commandLine: string,
readonly sessionId: string,
commandId?: string
preventShellHistory: boolean,
commandId?: string,
) {
super();

this._startMarker = this._register(this._xterm.raw.registerMarker());
this.instance.runCommand(this._commandLine, true, commandId);
// Prefix with space to exclude from shell history (requires HISTCONTROL=ignorespace
// or HIST_IGNORE_SPACE=1 env var which is set when the terminal is created)
const commandToSend = preventShellHistory ? ` ${this._commandLine}` : this._commandLine;
this.instance.runCommand(commandToSend, true, commandId);
}
getOutput(marker?: IXtermMarker): string {
return getOutput(this.instance, marker ?? this._startMarker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const enum TerminalChatAgentToolsSettingId {
ShellIntegrationTimeout = 'chat.tools.terminal.shellIntegrationTimeout',
AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts',
OutputLocation = 'chat.tools.terminal.outputLocation',
PreventShellHistory = 'chat.tools.terminal.preventShellHistory',

TerminalProfileLinux = 'chat.tools.terminal.terminalProfile.linux',
TerminalProfileMacOs = 'chat.tools.terminal.terminalProfile.osx',
Expand Down Expand Up @@ -445,6 +446,11 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
experiment: {
mode: 'auto'
}
},
[TerminalChatAgentToolsSettingId.PreventShellHistory]: {
type: 'boolean',
default: true,
markdownDescription: localize('preventShellHistory.description', "Whether to exclude commands run by the terminal tool from the shell history. Commands are prefixed with a space and the shell is configured to ignore space-prefixed commands. Supported shells:\n- bash: `HISTCONTROL=ignorespace`\n- zsh: `setopt HIST_IGNORE_SPACE`\n- fish: `fish_private_mode`\n- pwsh: PSReadLine `AddToHistoryHandler`"),
}
};

Expand Down
Loading