mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 09:43:49 +01:00
Merge pull request #285599 from microsoft/tyriar/276716_history
Add preventShellHistory feature
This commit is contained in:
@@ -62,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"
|
||||
|
||||
@@ -98,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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
# 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 -g envVarsToReport
|
||||
if test -n "$__vscode_shell_env_reporting"
|
||||
set envVarsToReport (string split "," "$__vscode_shell_env_reporting")
|
||||
|
||||
@@ -259,4 +259,13 @@ function Set-MappedKeyHandlers {
|
||||
|
||||
if ($Global:__VSCodeState.HasPSReadLine) {
|
||||
Set-MappedKeyHandlers
|
||||
|
||||
# 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
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -33,6 +33,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));
|
||||
|
||||
+27
-7
@@ -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 { 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';
|
||||
@@ -17,6 +18,8 @@ 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';
|
||||
import { isBash, isFish, isPowerShell, isZsh } from './runInTerminalHelpers.js';
|
||||
|
||||
const enum ShellLaunchType {
|
||||
Unknown = 0,
|
||||
@@ -50,8 +53,8 @@ export class ToolTerminalCreator {
|
||||
) {
|
||||
}
|
||||
|
||||
async createTerminal(shellOrProfile: string | ITerminalProfile, token: CancellationToken): Promise<IToolTerminal> {
|
||||
const instance = await this._createCopilotTerminal(shellOrProfile);
|
||||
async createTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem, token: CancellationToken): Promise<IToolTerminal> {
|
||||
const instance = await this._createCopilotTerminal(shellOrProfile, os);
|
||||
const toolTerminal: IToolTerminal = {
|
||||
instance,
|
||||
shellIntegrationQuality: ShellIntegrationQuality.None,
|
||||
@@ -138,15 +141,32 @@ export class ToolTerminalCreator {
|
||||
}
|
||||
}
|
||||
|
||||
private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile) {
|
||||
private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem) {
|
||||
const shellPath = isString(shellOrProfile) ? shellOrProfile : shellOrProfile.path;
|
||||
|
||||
const env: Record<string, string> = {
|
||||
// Avoid making `git diff` interactive when called from copilot
|
||||
GIT_PAGER: 'cat',
|
||||
};
|
||||
|
||||
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 = {
|
||||
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)) {
|
||||
|
||||
+5
-3
@@ -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 = await Promise.all(subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, options.shell, options.os, options.cwd, options.chatSessionId)));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(options.commandLine, options.chatSessionId);
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(trimmedCommandLine, options.chatSessionId);
|
||||
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 {
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 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(
|
||||
@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;
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -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))),
|
||||
@@ -792,7 +794,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
private async _initBackgroundTerminal(chatSessionId: string, termId: string, terminalToolSessionId: string | undefined, token: CancellationToken): Promise<IToolTerminal> {
|
||||
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);
|
||||
@@ -814,7 +817,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);
|
||||
@@ -957,7 +961,7 @@ class BackgroundTerminalExecution extends Disposable {
|
||||
private readonly _xterm: XtermTerminal,
|
||||
private readonly _commandLine: string,
|
||||
readonly sessionId: string,
|
||||
commandId?: string
|
||||
commandId?: string,
|
||||
) {
|
||||
super();
|
||||
|
||||
|
||||
+12
@@ -21,6 +21,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',
|
||||
@@ -455,6 +456,17 @@ 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. See below for the supported shells and the method used for each:"),
|
||||
`- \`bash\`: ${localize('preventShellHistory.description.bash', "Sets `HISTCONTROL=ignorespace` and prepends the command with space")}`,
|
||||
`- \`zsh\`: ${localize('preventShellHistory.description.zsh', "Sets `HIST_IGNORE_SPACE` option and prepends the command with space")}`,
|
||||
`- \`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'),
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user