From e44b0115d69eae0e440176a2003a6b8493d782ed Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Jan 2026 14:59:46 -0800 Subject: [PATCH 1/9] Add preventShellHistory feature Fixes #276716 --- .../common/scripts/shellIntegration-bash.sh | 7 +++++ .../common/scripts/shellIntegration-rc.zsh | 7 +++++ .../common/scripts/shellIntegration.fish | 7 +++++ .../common/scripts/shellIntegration.ps1 | 10 +++++++ .../executeStrategy/basicExecuteStrategy.ts | 9 ++++++- .../executeStrategy/noneExecuteStrategy.ts | 9 ++++++- .../executeStrategy/richExecuteStrategy.ts | 9 ++++++- .../browser/toolTerminalCreator.ts | 27 ++++++++++++++++--- .../browser/tools/runInTerminalTool.ts | 11 +++++--- .../terminalChatAgentToolsConfiguration.ts | 6 +++++ 10 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 8ec742e124f4..cefde90bfa0e 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -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 diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index f32b74f1927b..ae992b4cea11 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -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 diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index 51e0d3a7d6eb..50eb5127ab24 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -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") diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 3c37defc7b28..7fd647c5a4f7 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -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 } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index 25e6f69fc8fd..c5038c563b73 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -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 @@ -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, ) { } @@ -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 diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index 523906025b7a..dad28499d065 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -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 @@ -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, ) { } @@ -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'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index c65b53492d6d..7a3edd1461bf 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -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 @@ -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, ) { } @@ -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'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts index b18ccbcf0a60..d04b1c11372a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts @@ -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'; @@ -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, @@ -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 = { + // 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)) { 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 e46f742d4309..5e7cefa61a84 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -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)); @@ -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); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 68f4bc6390d7..d2df2f777184 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -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', @@ -445,6 +446,11 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Fri, 2 Jan 2026 04:51:03 -0800 Subject: [PATCH 2/9] Update src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/terminal/common/scripts/shellIntegration.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 7fd647c5a4f7..e24ee429da93 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -263,10 +263,6 @@ if ($Global:__VSCodeState.HasPSReadLine) { # 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 } From 98268eb8f3185c721b8493943265b1d34108bb1a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 04:59:09 -0800 Subject: [PATCH 3/9] Improve preventShellHistory setting description --- .../common/terminalChatAgentToolsConfiguration.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index d2df2f777184..9c700d1dcecc 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -450,7 +450,13 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Fri, 2 Jan 2026 05:03:36 -0800 Subject: [PATCH 4/9] Prevent any history in pwsh --- .../contrib/terminal/common/scripts/shellIntegration.ps1 | 6 +++++- .../common/terminalChatAgentToolsConfiguration.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index e24ee429da93..7694b604922e 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -260,9 +260,13 @@ function Set-MappedKeyHandlers { if ($Global:__VSCodeState.HasPSReadLine) { Set-MappedKeyHandlers - # Configure history exclusion for space-prefixed commands when requested by VS Code + # Configure history exclusion 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 $false + } } $env:VSCODE_EXCLUDE_FROM_HISTORY = $null } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index 9c700d1dcecc..ac2d6fb9a923 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -455,7 +455,7 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Fri, 2 Jan 2026 05:04:00 -0800 Subject: [PATCH 5/9] Align env var with feature name --- .../contrib/terminal/common/scripts/shellIntegration-bash.sh | 4 ++-- .../contrib/terminal/common/scripts/shellIntegration-rc.zsh | 4 ++-- .../contrib/terminal/common/scripts/shellIntegration.fish | 4 ++-- .../contrib/terminal/common/scripts/shellIntegration.ps1 | 4 ++-- .../chatAgentTools/browser/toolTerminalCreator.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index cefde90bfa0e..d64c11d3925c 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -12,10 +12,10 @@ 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 +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then export HISTCONTROL="ignorespace" fi -unset VSCODE_EXCLUDE_FROM_HISTORY +unset VSCODE_PREVENT_SHELL_HISTORY vsc_env_keys=() vsc_env_values=() diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index ae992b4cea11..d8097e9fdbe3 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -16,10 +16,10 @@ 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 +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then setopt HIST_IGNORE_SPACE fi -unset VSCODE_EXCLUDE_FROM_HISTORY +unset VSCODE_PREVENT_SHELL_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 diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index 50eb5127ab24..f4fc8683e8e1 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -26,10 +26,10 @@ 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" +if test "$VSCODE_PREVENT_SHELL_HISTORY" = "1" set -g fish_private_mode 1 end -set -e VSCODE_EXCLUDE_FROM_HISTORY +set -e VSCODE_PREVENT_SHELL_HISTORY set -g envVarsToReport if test -n "$__vscode_shell_env_reporting" set envVarsToReport (string split "," "$__vscode_shell_env_reporting") diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 7694b604922e..77bb1a8befbf 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -262,11 +262,11 @@ if ($Global:__VSCodeState.HasPSReadLine) { # Configure history exclusion 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") { + if ($env:VSCODE_PREVENT_SHELL_HISTORY -eq "1") { Set-PSReadLineOption -AddToHistoryHandler { param([string]$line) return $false } } - $env:VSCODE_EXCLUDE_FROM_HISTORY = $null + $env:VSCODE_PREVENT_SHELL_HISTORY = $null } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts index d04b1c11372a..42579db8f07e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts @@ -156,9 +156,9 @@ export class ToolTerminalCreator { // 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. + // VSCODE_PREVENT_SHELL_HISTORY=1 is handled by shell integration scripts for all shells. if (preventShellHistory && shellSupportsHistoryExclusion) { - env['VSCODE_EXCLUDE_FROM_HISTORY'] = '1'; + env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; } const config: IShellLaunchConfig = { From ddfe1c50cba9dcf27246fc5da1c1aab50e3c4164 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:07:03 -0800 Subject: [PATCH 6/9] Tidy up execute strategy --- .../browser/executeStrategy/basicExecuteStrategy.ts | 9 +++++---- .../browser/executeStrategy/noneExecuteStrategy.ts | 9 +++++---- .../browser/executeStrategy/richExecuteStrategy.ts | 3 ++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index c5038c563b73..5b783a2559aa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -116,6 +116,11 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { await waitForIdle(this._instance.onData, 100); } + // 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; + // Execute the command if (commandId) { this._log(`In basic execute strategy: skipping pre-bound command id ${commandId} because basic shell integration executes via sendText`); @@ -124,10 +129,6 @@ 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(commandToSend, true); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index dad28499d065..3e524231b2d5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -74,14 +74,15 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { await waitForIdle(this._instance.onData, 100); } - // Execute the command - // 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; + + // Execute the command + // 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) this._log(`Executing command line \`${commandLine}\``); this._instance.sendText(commandToSend, true); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index 7a3edd1461bf..6855c91641aa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -78,11 +78,12 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { this._log.bind(this) ); - // 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; + + // Execute the command this._log(`Executing command line \`${commandLine}\``); this._instance.runCommand(commandToSend, true, commandId); From 7a2e8dd8e4c3489b42ad072c1fe0d7511c6724c5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 05:19:51 -0800 Subject: [PATCH 7/9] Move logic into rewriter, use helpers for shell determination --- .../executeStrategy/basicExecuteStrategy.ts | 10 +---- .../executeStrategy/noneExecuteStrategy.ts | 10 +---- .../executeStrategy/richExecuteStrategy.ts | 10 +---- .../browser/runInTerminalHelpers.ts | 7 ++++ .../browser/toolTerminalCreator.ts | 12 +++--- .../commandLinePreventHistoryRewriter.ts | 38 +++++++++++++++++++ .../browser/tools/runInTerminalTool.ts | 8 +++- 7 files changed, 60 insertions(+), 35 deletions(-) create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index 5b783a2559aa..25e6f69fc8fd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -8,14 +8,12 @@ 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 @@ -51,7 +49,6 @@ 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, ) { } @@ -116,11 +113,6 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { await waitForIdle(this._instance.onData, 100); } - // 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; - // Execute the command if (commandId) { this._log(`In basic execute strategy: skipping pre-bound command id ${commandId} because basic shell integration executes via sendText`); @@ -130,7 +122,7 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { // ^C being sent and also to return the exit code of 130 when from the shell when that // occurs. this._log(`Executing command line \`${commandLine}\``); - this._instance.sendText(commandToSend, true); + this._instance.sendText(commandLine, true); // Wait for the next end execution event - note that this may not correspond to the actual // execution requested diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index 3e524231b2d5..523906025b7a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -7,13 +7,11 @@ 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 @@ -32,7 +30,6 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { constructor( private readonly _instance: ITerminalInstance, private readonly _hasReceivedUserInput: () => boolean, - @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { } @@ -74,17 +71,12 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { await waitForIdle(this._instance.onData, 100); } - // 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; - // Execute the command // 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) this._log(`Executing command line \`${commandLine}\``); - this._instance.sendText(commandToSend, true); + this._instance.sendText(commandLine, true); // Assume the command is done when it's idle this._log('Waiting for idle with prompt heuristics'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index 6855c91641aa..c65b53492d6d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -8,14 +8,12 @@ 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 @@ -34,7 +32,6 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { constructor( private readonly _instance: ITerminalInstance, private readonly _commandDetection: ICommandDetectionCapability, - @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { } @@ -78,14 +75,9 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { this._log.bind(this) ); - // 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; - // Execute the command this._log(`Executing command line \`${commandLine}\``); - this._instance.runCommand(commandToSend, true, commandId); + this._instance.runCommand(commandLine, true, commandId); // Wait for the terminal to idle this._log('Waiting for done event'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index fbbc47ec2b70..dd68e390ffaa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -32,6 +32,13 @@ export function isZsh(envShell: string, os: OperatingSystem): boolean { return /^zsh$/.test(pathPosix.basename(envShell)); } +export function isBash(envShell: string, os: OperatingSystem): boolean { + if (os === OperatingSystem.Windows) { + return /^bash(?:\.exe)?$/i.test(pathWin32.basename(envShell)); + } + return /^bash$/.test(pathPosix.basename(envShell)); +} + export function isFish(envShell: string, os: OperatingSystem): boolean { if (os === OperatingSystem.Windows) { return /^fish(?:\.exe)?$/i.test(pathWin32.basename(envShell)); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts index 42579db8f07e..b9de825962fd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts @@ -9,7 +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 { OperatingSystem } from '../../../../../base/common/platform.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'; @@ -19,6 +19,7 @@ import { ITerminalLogService, ITerminalProfile, TerminalSettingId, type IShellLa import { ITerminalService, type ITerminalInstance } from '../../../terminal/browser/terminal.js'; import { getShellIntegrationTimeout } from '../../../terminal/common/terminalEnvironment.js'; import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; +import { isBash, isFish, isPowerShell, isZsh } from './runInTerminalHelpers.js'; const enum ShellLaunchType { Unknown = 0, @@ -52,8 +53,8 @@ export class ToolTerminalCreator { ) { } - async createTerminal(shellOrProfile: string | ITerminalProfile, token: CancellationToken): Promise { - const instance = await this._createCopilotTerminal(shellOrProfile); + async createTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem, token: CancellationToken): Promise { + const instance = await this._createCopilotTerminal(shellOrProfile, os); const toolTerminal: IToolTerminal = { instance, shellIntegrationQuality: ShellIntegrationQuality.None, @@ -140,12 +141,11 @@ export class ToolTerminalCreator { } } - private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile) { + private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem) { 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 shellSupportsHistoryExclusion = isBash(shellPath, os) || isZsh(shellPath, os) || isFish(shellPath, os) || isPowerShell(shellPath, os); const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; const env: Record = { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts new file mode 100644 index 000000000000..797ecaf86296 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { isBash, isZsh } from '../../runInTerminalHelpers.js'; +import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAgentToolsConfiguration.js'; +import type { ICommandLineRewriter, ICommandLineRewriterOptions, ICommandLineRewriterResult } from './commandLineRewriter.js'; + +/** + * Rewriter that prepends a space to commands to prevent them from being added to shell history. + * This requires HISTCONTROL=ignorespace or HIST_IGNORE_SPACE=1 env var which is set when the + * terminal is created. + */ +export class CommandLinePreventHistoryRewriter extends Disposable implements ICommandLineRewriter { + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(); + } + + rewrite(options: ICommandLineRewriterOptions): ICommandLineRewriterResult | undefined { + const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; + if (!preventShellHistory) { + return undefined; + } + // Only bash and zsh use space prefix to exclude from history + if (isBash(options.shell, options.os) || isZsh(options.shell, options.os)) { + return { + rewritten: ` ${options.commandLine}`, + reasoning: 'Prepended with a space to exclude from shell history' + }; + } + return undefined; + } +} 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 5e7cefa61a84..35c7315ce2a1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -47,6 +47,7 @@ import { IPollingResult, OutputMonitorState } from './monitoring/types.js'; import { LocalChatSessionUri } from '../../../../chat/common/model/chatUri.js'; import type { ICommandLineRewriter } from './commandLineRewriter/commandLineRewriter.js'; import { CommandLineCdPrefixRewriter } from './commandLineRewriter/commandLineCdPrefixRewriter.js'; +import { CommandLinePreventHistoryRewriter } from './commandLineRewriter/commandLinePreventHistoryRewriter.js'; import { CommandLinePwshChainOperatorRewriter } from './commandLineRewriter/commandLinePwshChainOperatorRewriter.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IHistoryService } from '../../../../../services/history/common/history.js'; @@ -312,6 +313,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._commandLineRewriters = [ this._register(this._instantiationService.createInstance(CommandLineCdPrefixRewriter)), this._register(this._instantiationService.createInstance(CommandLinePwshChainOperatorRewriter, this._treeSitterCommandParser)), + this._register(this._instantiationService.createInstance(CommandLinePreventHistoryRewriter)), ]; this._commandLineAnalyzers = [ this._register(this._instantiationService.createInstance(CommandLineFileWriteAnalyzer, this._treeSitterCommandParser, (message, args) => this._logService.info(`RunInTerminalTool#CommandLineFileWriteAnalyzer: ${message}`, args))), @@ -793,7 +795,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { private async _initBackgroundTerminal(chatSessionId: string, termId: string, terminalToolSessionId: string | undefined, token: CancellationToken): Promise { this._logService.debug(`RunInTerminalTool: Creating background terminal with ID=${termId}`); const profile = await this._profileFetcher.getCopilotProfile(); - const toolTerminal = await this._terminalToolCreator.createTerminal(profile, token); + const os = await this._osBackend; + const toolTerminal = await this._terminalToolCreator.createTerminal(profile, os, token); this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance); this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionId, toolTerminal.instance); this._registerInputListener(toolTerminal); @@ -815,7 +818,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { return cachedTerminal; } const profile = await this._profileFetcher.getCopilotProfile(); - const toolTerminal = await this._terminalToolCreator.createTerminal(profile, token); + const os = await this._osBackend; + const toolTerminal = await this._terminalToolCreator.createTerminal(profile, os, token); this._terminalChatService.registerTerminalInstanceWithToolSession(terminalToolSessionId, toolTerminal.instance); this._terminalChatService.registerTerminalInstanceWithChatSession(chatSessionId, toolTerminal.instance); this._registerInputListener(toolTerminal); From a4f45b6ff156d8e68672f60bc571e5d569365707 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:42:08 -0800 Subject: [PATCH 8/9] Address feedback, remove redundancy and improve comments --- .../browser/toolTerminalCreator.ts | 21 ++++++++++--------- .../commandLineAutoApproveAnalyzer.ts | 8 ++++--- .../commandLinePreventHistoryRewriter.ts | 7 ++++--- .../browser/tools/runInTerminalTool.ts | 9 ++------ 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts index b9de825962fd..bb0ea29770e4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts @@ -144,21 +144,22 @@ export class ToolTerminalCreator { private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem) { const shellPath = isString(shellOrProfile) ? shellOrProfile : shellOrProfile.path; - // Check if the shell supports history exclusion via shell integration scripts - const shellSupportsHistoryExclusion = isBash(shellPath, os) || isZsh(shellPath, os) || isFish(shellPath, os) || isPowerShell(shellPath, os); - const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; - const env: Record = { // 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_PREVENT_SHELL_HISTORY=1 is handled by shell integration scripts for all shells. - if (preventShellHistory && shellSupportsHistoryExclusion) { - env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; + const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; + if (preventShellHistory) { + // Check if the shell supports history exclusion via shell integration scripts + if ( + isBash(shellPath, os) || + isZsh(shellPath, os) || + isFish(shellPath, os) || + isPowerShell(shellPath, os) + ) { + env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; + } } const config: IShellLaunchConfig = { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts index ae37e9732464..f85145c6dcc6 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts @@ -67,9 +67,11 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma }; } + const trimmedCommandLine = options.commandLine.trimStart(); + let subCommands: string[] | undefined; try { - subCommands = await this._treeSitterCommandParser.extractSubCommands(options.treeSitterLanguage, options.commandLine); + subCommands = await this._treeSitterCommandParser.extractSubCommands(options.treeSitterLanguage, trimmedCommandLine); this._log(`Parsed sub-commands via ${options.treeSitterLanguage} grammar`, subCommands); } catch (e) { console.error(e); @@ -88,7 +90,7 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma } const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os)); - const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine); + const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(trimmedCommandLine); const autoApproveReasons: string[] = [ ...subCommandResults.map(e => e.reason), commandLineResult.reason, @@ -169,7 +171,7 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma } if (!isAutoApproved && isAutoApproveEnabled) { - customActions = generateAutoApproveActions(options.commandLine, subCommands, { subCommandResults, commandLineResult }); + customActions = generateAutoApproveActions(trimmedCommandLine, subCommands, { subCommandResults, commandLineResult }); } return { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts index 797ecaf86296..51452bac4349 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineRewriter/commandLinePreventHistoryRewriter.ts @@ -10,9 +10,10 @@ import { TerminalChatAgentToolsSettingId } from '../../../common/terminalChatAge import type { ICommandLineRewriter, ICommandLineRewriterOptions, ICommandLineRewriterResult } from './commandLineRewriter.js'; /** - * Rewriter that prepends a space to commands to prevent them from being added to shell history. - * This requires HISTCONTROL=ignorespace or HIST_IGNORE_SPACE=1 env var which is set when the - * terminal is created. + * Rewriter that prepends a space to commands to prevent them from being added to shell history for + * certain shells. This depends on $VSCODE_PREVENT_SHELL_HISTORY being handled in shell integration + * scripts to set `HISTCONTROL=ignorespace` (bash) or `HIST_IGNORE_SPACE` (zsh) env vars. The + * prepended space is harmless so we don't try to remove it if shell integration isn't functional. */ export class CommandLinePreventHistoryRewriter extends Disposable implements ICommandLineRewriter { constructor( 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 35c7315ce2a1..a0af0224abef 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -563,8 +563,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { let pollingResult: IPollingResult & { pollDurationMs: number } | undefined; try { this._logService.debug(`RunInTerminalTool: Starting background execution \`${command}\``); - const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; - const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId, preventShellHistory, commandId); + const execution = new BackgroundTerminalExecution(toolTerminal.instance, xterm, command, chatSessionId, commandId); RunInTerminalTool._backgroundExecutions.set(termId, execution); outputMonitor = store.add(this._instantiationService.createInstance(OutputMonitor, execution, undefined, invocation.context!, token, command)); @@ -962,16 +961,12 @@ class BackgroundTerminalExecution extends Disposable { private readonly _xterm: XtermTerminal, private readonly _commandLine: string, readonly sessionId: string, - preventShellHistory: boolean, commandId?: string, ) { super(); this._startMarker = this._register(this._xterm.raw.registerMarker()); - // 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); + this.instance.runCommand(this._commandLine, true, commandId); } getOutput(marker?: IXtermMarker): string { return getOutput(this.instance, marker ?? this._startMarker); From ba793786bd68cafed859aeb95b73d683a0574d77 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:46:24 -0800 Subject: [PATCH 9/9] Clean up shell integration scripts --- .../common/scripts/shellIntegration-bash.sh | 13 ++++++------- .../terminal/common/scripts/shellIntegration-rc.zsh | 13 ++++++------- .../terminal/common/scripts/shellIntegration.fish | 6 +++--- .../terminal/common/scripts/shellIntegration.ps1 | 5 ++--- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index d64c11d3925c..87e3a63fe0a4 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -10,13 +10,6 @@ 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_PREVENT_SHELL_HISTORY:-}" = "1" ]; then - export HISTCONTROL="ignorespace" -fi -unset VSCODE_PREVENT_SHELL_HISTORY - vsc_env_keys=() vsc_env_values=() use_associative_array=0 @@ -69,6 +62,12 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +# Prevent AI-executed commands from polluting shell history +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then + export HISTCONTROL="ignorespace" + builtin unset VSCODE_PREVENT_SHELL_HISTORY +fi + # Apply EnvironmentVariableCollections if needed if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index d8097e9fdbe3..5389bd95b12d 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -14,13 +14,6 @@ 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_PREVENT_SHELL_HISTORY:-}" = "1" ]; then - setopt HIST_IGNORE_SPACE -fi -unset VSCODE_PREVENT_SHELL_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 @@ -105,6 +98,12 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +# Prevent AI-executed commands from polluting shell history +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then + builtin setopt HIST_IGNORE_SPACE + builtin unset VSCODE_PREVENT_SHELL_HISTORY +fi + # The property (P) and command (E) codes embed values which require escaping. # Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. __vsc_escape_value() { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index f4fc8683e8e1..0e0b6798c168 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -24,12 +24,12 @@ 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 +# Prevent AI-executed commands from polluting shell history if test "$VSCODE_PREVENT_SHELL_HISTORY" = "1" set -g fish_private_mode 1 + set -e VSCODE_PREVENT_SHELL_HISTORY end -set -e VSCODE_PREVENT_SHELL_HISTORY + set -g envVarsToReport if test -n "$__vscode_shell_env_reporting" set envVarsToReport (string split "," "$__vscode_shell_env_reporting") diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 77bb1a8befbf..e89f6ec24c45 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -260,13 +260,12 @@ function Set-MappedKeyHandlers { if ($Global:__VSCodeState.HasPSReadLine) { Set-MappedKeyHandlers - # Configure history exclusion when requested by VS Code - # This is used by Copilot terminals to prevent AI-executed commands from polluting history + # Prevent AI-executed commands from polluting shell history if ($env:VSCODE_PREVENT_SHELL_HISTORY -eq "1") { Set-PSReadLineOption -AddToHistoryHandler { param([string]$line) return $false } + $env:VSCODE_PREVENT_SHELL_HISTORY = $null } - $env:VSCODE_PREVENT_SHELL_HISTORY = $null }