mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-28 14:47:53 +00:00
Merge pull request #272281 from microsoft/tyriar/tree_sitter_subcommand
Use tree sitter for parsing terminal auto approve sub-commands
This commit is contained in:
commit
cc5c66b2e1
@ -1824,6 +1824,7 @@ export default tseslint.config(
|
||||
// terminalContrib is one extra folder deep
|
||||
'vs/workbench/contrib/terminalContrib/*/~',
|
||||
'vscode-notebook-renderer', // Type only import
|
||||
'@vscode/tree-sitter-wasm', // type import
|
||||
{
|
||||
'when': 'hasBrowser',
|
||||
'pattern': '@xterm/xterm'
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -22,7 +22,7 @@
|
||||
"@vscode/spdlog": "^0.15.2",
|
||||
"@vscode/sqlite3": "5.1.8-vscode",
|
||||
"@vscode/sudo-prompt": "9.3.1",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/tree-sitter-wasm": "^0.2.0",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@vscode/windows-mutex": "^0.5.0",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
@ -3319,9 +3319,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/tree-sitter-wasm": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.4.tgz",
|
||||
"integrity": "sha512-kQVVg/CamCYDM+/XYCZuNTQyixjZd8ts/Gf84UzjEY0eRnbg6kiy5I9z2/2i3XdqwhI87iG07rkMR2KwhqcSbA==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.2.0.tgz",
|
||||
"integrity": "sha512-abvLfKwmriqgdS4WrIzFK7mzdPUVqIIW1UWarp2lA8lpOZ1EDPL1snRBKe7g+5R5ri173mNJEuPLnG/NlpMp4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/v8-heap-parser": {
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
"@vscode/spdlog": "^0.15.2",
|
||||
"@vscode/sqlite3": "5.1.8-vscode",
|
||||
"@vscode/sudo-prompt": "9.3.1",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/tree-sitter-wasm": "^0.2.0",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@vscode/windows-mutex": "^0.5.0",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
|
||||
8
remote/package-lock.json
generated
8
remote/package-lock.json
generated
@ -16,7 +16,7 @@
|
||||
"@vscode/proxy-agent": "^0.35.0",
|
||||
"@vscode/ripgrep": "^1.15.13",
|
||||
"@vscode/spdlog": "^0.15.2",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/tree-sitter-wasm": "^0.2.0",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
"@vscode/windows-registry": "^1.1.0",
|
||||
@ -185,9 +185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vscode/tree-sitter-wasm": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.4.tgz",
|
||||
"integrity": "sha512-kQVVg/CamCYDM+/XYCZuNTQyixjZd8ts/Gf84UzjEY0eRnbg6kiy5I9z2/2i3XdqwhI87iG07rkMR2KwhqcSbA==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.2.0.tgz",
|
||||
"integrity": "sha512-abvLfKwmriqgdS4WrIzFK7mzdPUVqIIW1UWarp2lA8lpOZ1EDPL1snRBKe7g+5R5ri173mNJEuPLnG/NlpMp4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/vscode-languagedetection": {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"@vscode/proxy-agent": "^0.35.0",
|
||||
"@vscode/ripgrep": "^1.15.13",
|
||||
"@vscode/spdlog": "^0.15.2",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/tree-sitter-wasm": "^0.2.0",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
"@vscode/windows-registry": "^1.1.0",
|
||||
|
||||
8
remote/web/package-lock.json
generated
8
remote/web/package-lock.json
generated
@ -11,7 +11,7 @@
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/tree-sitter-wasm": "^0.2.0",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.114",
|
||||
"@xterm/addon-image": "^0.9.0-beta.131",
|
||||
@ -78,9 +78,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/tree-sitter-wasm": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.1.4.tgz",
|
||||
"integrity": "sha512-kQVVg/CamCYDM+/XYCZuNTQyixjZd8ts/Gf84UzjEY0eRnbg6kiy5I9z2/2i3XdqwhI87iG07rkMR2KwhqcSbA==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.2.0.tgz",
|
||||
"integrity": "sha512-abvLfKwmriqgdS4WrIzFK7mzdPUVqIIW1UWarp2lA8lpOZ1EDPL1snRBKe7g+5R5ri173mNJEuPLnG/NlpMp4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vscode/vscode-languagedetection": {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@vscode/iconv-lite-umd": "0.7.1",
|
||||
"@vscode/tree-sitter-wasm": "^0.1.4",
|
||||
"@vscode/tree-sitter-wasm": "^0.2.0",
|
||||
"@vscode/vscode-languagedetection": "1.0.21",
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.114",
|
||||
"@xterm/addon-image": "^0.9.0-beta.131",
|
||||
|
||||
@ -54,7 +54,7 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend {
|
||||
}
|
||||
|
||||
const currentLanguage = this._languageIdObs.read(reader);
|
||||
const treeSitterLang = this._treeSitterLibraryService.getLanguage(currentLanguage, reader);
|
||||
const treeSitterLang = this._treeSitterLibraryService.getLanguage(currentLanguage, false, reader);
|
||||
if (!treeSitterLang) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -12,20 +12,46 @@ export const ITreeSitterLibraryService = createDecorator<ITreeSitterLibraryServi
|
||||
export interface ITreeSitterLibraryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Gets the tree sitter Parser constructor.
|
||||
*/
|
||||
getParserClass(): Promise<typeof Parser>;
|
||||
|
||||
supportsLanguage(languageId: string, reader: IReader | undefined): boolean;
|
||||
getLanguage(languageId: string, reader: IReader | undefined): Language | undefined;
|
||||
/**
|
||||
* Return value of null indicates that there are no injection queries for this language.
|
||||
* @param languageId
|
||||
* @param reader
|
||||
* Checks whether a language is supported and available based setting enablement.
|
||||
* @param languageId The language identifier to check.
|
||||
* @param reader Optional observable reader.
|
||||
*/
|
||||
supportsLanguage(languageId: string, reader: IReader | undefined): boolean;
|
||||
|
||||
/**
|
||||
* Gets the tree sitter Language object synchronously.
|
||||
* @param languageId The language identifier to retrieve.
|
||||
* @param ignoreSupportsCheck Whether to ignore the supportsLanguage check.
|
||||
* @param reader Optional observable reader.
|
||||
*/
|
||||
getLanguage(languageId: string, ignoreSupportsCheck: boolean, reader: IReader | undefined): Language | undefined;
|
||||
|
||||
/**
|
||||
* Gets the injection queries for a language. A return value of `null`
|
||||
* indicates that there are no highlights queries for this language.
|
||||
* @param languageId The language identifier to retrieve queries for.
|
||||
* @param reader Optional observable reader.
|
||||
*/
|
||||
getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined;
|
||||
|
||||
/**
|
||||
* Return value of null indicates that there are no highlights queries for this language.
|
||||
* @param languageId
|
||||
* @param reader
|
||||
* Gets the highlighting queries for a language. A return value of `null`
|
||||
* indicates that there are no highlights queries for this language.
|
||||
* @param languageId The language identifier to retrieve queries for.
|
||||
* @param reader Optional observable reader.
|
||||
*/
|
||||
getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined;
|
||||
|
||||
/**
|
||||
* Creates a one-off custom query for a language.
|
||||
* @param language The Language to create the query for.
|
||||
* @param querySource The query source string to compile.
|
||||
*/
|
||||
createQuery(language: Language, querySource: string): Promise<Query>;
|
||||
}
|
||||
|
||||
@ -11,30 +11,26 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getParserClass(): Promise<typeof Parser> {
|
||||
throw new Error('getParserClass is not implemented in StandaloneTreeSitterLibraryService');
|
||||
throw new Error('not implemented in StandaloneTreeSitterLibraryService');
|
||||
}
|
||||
|
||||
supportsLanguage(languageId: string, reader: IReader | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getLanguage(languageId: string, reader: IReader | undefined): Language | undefined {
|
||||
getLanguage(languageId: string, ignoreSupportsCheck: boolean, reader: IReader | undefined): Language | undefined {
|
||||
return undefined;
|
||||
}
|
||||
/**
|
||||
* Return value of null indicates that there are no injection queries for this language.
|
||||
* @param languageId
|
||||
* @param reader
|
||||
*/
|
||||
|
||||
getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Return value of null indicates that there are no highlights queries for this language.
|
||||
* @param languageId
|
||||
* @param reader
|
||||
*/
|
||||
|
||||
getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined {
|
||||
return null;
|
||||
}
|
||||
|
||||
async createQuery(language: Language, querySource: string): Promise<Query> {
|
||||
throw new Error('not implemented in StandaloneTreeSitterLibraryService');
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,14 +11,14 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getParserClass(): Promise<typeof Parser> {
|
||||
throw new Error('getParserClass is not implemented in TestTreeSitterLibraryService');
|
||||
throw new Error('not implemented in TestTreeSitterLibraryService');
|
||||
}
|
||||
|
||||
supportsLanguage(languageId: string, reader: IReader | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getLanguage(languageId: string, reader: IReader | undefined): Language | undefined {
|
||||
getLanguage(languageId: string, ignoreSupportsCheck: boolean, reader: IReader | undefined): Language | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -29,4 +29,8 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService {
|
||||
getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined {
|
||||
return null;
|
||||
}
|
||||
|
||||
async createQuery(language: Language, querySource: string): Promise<Query> {
|
||||
throw new Error('not implemented in TestTreeSitterLibraryService');
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,7 +69,10 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str
|
||||
|
||||
// We shouldn't offer configuring rules for commands that are explicitly denied since it
|
||||
// wouldn't get auto approved with a new rule
|
||||
const canCreateAutoApproval = autoApproveResult.subCommandResults.some(e => e.result !== 'denied') || autoApproveResult.commandLineResult.result === 'denied';
|
||||
const canCreateAutoApproval = (
|
||||
autoApproveResult.subCommandResults.every(e => e.result !== 'denied') &&
|
||||
autoApproveResult.commandLineResult.result !== 'denied'
|
||||
);
|
||||
if (canCreateAutoApproval) {
|
||||
const unapprovedSubCommands = subCommands.filter((_, index) => {
|
||||
return autoApproveResult.subCommandResults[index].result !== 'approved';
|
||||
@ -152,9 +155,7 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str
|
||||
if (
|
||||
firstSubcommandFirstWord !== commandLine &&
|
||||
!commandsWithSubcommands.has(commandLine) &&
|
||||
!commandsWithSubSubCommands.has(commandLine) &&
|
||||
autoApproveResult.commandLineResult.result !== 'denied' &&
|
||||
autoApproveResult.subCommandResults.every(e => e.result !== 'denied')
|
||||
!commandsWithSubSubCommands.has(commandLine)
|
||||
) {
|
||||
actions.push({
|
||||
label: localize('autoApprove.exactCommand', 'Always Allow Exact Command Line'),
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { OperatingSystem } from '../../../../../base/common/platform.js';
|
||||
import { isPowerShell } from './runInTerminalHelpers.js';
|
||||
|
||||
function createNumberRange(start: number, end: number): string[] {
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => (start + i).toString());
|
||||
}
|
||||
|
||||
function sortByStringLengthDesc(arr: string[]): string[] {
|
||||
return [...arr].sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
// Derived from https://github.com/microsoft/vscode/blob/315b0949786b3807f05cb6acd13bf0029690a052/extensions/terminal-suggest/src/tokens.ts#L14-L18
|
||||
// Some of these can match the same string, so the order matters.
|
||||
//
|
||||
// This isn't perfect, at some point it would be better off moving over to tree sitter for this
|
||||
// instead of simple string matching.
|
||||
const shellTypeResetChars = new Map<'sh' | 'zsh' | 'pwsh', string[]>([
|
||||
['sh', sortByStringLengthDesc([
|
||||
// Redirection docs (bash) https://www.gnu.org/software/bash/manual/html_node/Redirections.html
|
||||
...createNumberRange(1, 9).concat('').map(n => `${n}<<<`), // Here strings
|
||||
...createNumberRange(1, 9).concat('').flatMap(n => createNumberRange(1, 9).map(m => `${n}>&${m}`)), // Redirect stream to stream
|
||||
...createNumberRange(1, 9).concat('').map(n => `${n}<>`), // Open file descriptor for reading and writing
|
||||
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>>`),
|
||||
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>`),
|
||||
'0<', '||', '&&', '|&', '<<', '&', ';', '{', '>', '<', '|'
|
||||
])],
|
||||
['zsh', sortByStringLengthDesc([
|
||||
// Redirection docs https://zsh.sourceforge.io/Doc/Release/Redirection.html
|
||||
...createNumberRange(1, 9).concat('').map(n => `${n}<<<`), // Here strings
|
||||
...createNumberRange(1, 9).concat('').flatMap(n => createNumberRange(1, 9).map(m => `${n}>&${m}`)), // Redirect stream to stream
|
||||
...createNumberRange(1, 9).concat('').map(n => `${n}<>`), // Open file descriptor for reading and writing
|
||||
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>>`),
|
||||
...createNumberRange(1, 9).concat('&', '').map(n => `${n}>`),
|
||||
'<(', '||', '>|', '>!', '&&', '|&', '&', ';', '{', '<(', '<', '|'
|
||||
])],
|
||||
['pwsh', sortByStringLengthDesc([
|
||||
// Redirection docs: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_redirection?view=powershell-7.5
|
||||
...createNumberRange(1, 6).concat('*', '').flatMap(n => createNumberRange(1, 6).map(m => `${n}>&${m}`)), // Stream to stream redirection
|
||||
...createNumberRange(1, 6).concat('*', '').map(n => `${n}>>`),
|
||||
...createNumberRange(1, 6).concat('*', '').map(n => `${n}>`),
|
||||
'&&', '<', '|', ';', '!', '&'
|
||||
])],
|
||||
]);
|
||||
|
||||
export function splitCommandLineIntoSubCommands(commandLine: string, envShell: string, envOS: OperatingSystem): string[] {
|
||||
let shellType: 'sh' | 'zsh' | 'pwsh';
|
||||
const envShellWithoutExe = envShell.replace(/\.exe$/, '');
|
||||
if (isPowerShell(envShell, envOS)) {
|
||||
shellType = 'pwsh';
|
||||
} else {
|
||||
switch (envShellWithoutExe) {
|
||||
case 'zsh': shellType = 'zsh'; break;
|
||||
default: shellType = 'sh'; break;
|
||||
}
|
||||
}
|
||||
const subCommands = [commandLine];
|
||||
const resetChars = shellTypeResetChars.get(shellType);
|
||||
if (resetChars) {
|
||||
for (const chars of resetChars) {
|
||||
for (let i = 0; i < subCommands.length; i++) {
|
||||
const subCommand = subCommands[i];
|
||||
if (subCommand.includes(chars)) {
|
||||
subCommands.splice(i, 1, ...subCommand.split(chars).map(e => e.trim()));
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return subCommands.filter(e => e.length > 0);
|
||||
}
|
||||
@ -43,10 +43,10 @@ import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js';
|
||||
import { getOutput } from '../outputHelpers.js';
|
||||
import { dedupeRules, generateAutoApproveActions, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js';
|
||||
import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js';
|
||||
import { splitCommandLineIntoSubCommands } from '../subCommands.js';
|
||||
import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js';
|
||||
import { OutputMonitor } from './monitoring/outputMonitor.js';
|
||||
import { IPollingResult, OutputMonitorState } from './monitoring/types.js';
|
||||
import { TreeSitterCommandParser, TreeSitterCommandParserLanguage } from '../treeSitterCommandParser.js';
|
||||
|
||||
// #region Tool data
|
||||
|
||||
@ -56,10 +56,9 @@ function createPowerShellModelDescription(shell: string): string {
|
||||
`This tool allows you to execute ${isWinPwsh ? 'Windows PowerShell 5.1' : 'PowerShell'} commands in a persistent terminal session, preserving environment variables, working directory, and other context across multiple commands.`,
|
||||
'',
|
||||
'Command Execution:',
|
||||
'- Does NOT support multi-line commands',
|
||||
`- ${isWinPwsh
|
||||
? 'Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly'
|
||||
: 'Use && to chain simple commands on one line'}`,
|
||||
// Even for pwsh 7+ we want to use `;` to chain commands since the tree sitter grammar
|
||||
// doesn't parse `&&`. See https://github.com/airbus-cert/tree-sitter-powershell/issues/27
|
||||
'- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly',
|
||||
'- Prefer pipelines | for object-based data flow',
|
||||
'',
|
||||
'Directory Management:',
|
||||
@ -259,9 +258,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
|
||||
private readonly _terminalToolCreator: ToolTerminalCreator;
|
||||
private readonly _commandSimplifier: CommandSimplifier;
|
||||
protected readonly _profileFetcher: TerminalProfileFetcher;
|
||||
private readonly _treeSitterCommandParser: TreeSitterCommandParser;
|
||||
private readonly _telemetry: RunInTerminalToolTelemetry;
|
||||
protected readonly _commandLineAutoApprover: CommandLineAutoApprover;
|
||||
protected readonly _profileFetcher: TerminalProfileFetcher;
|
||||
protected readonly _sessionTerminalAssociations: Map<string, IToolTerminal> = new Map();
|
||||
|
||||
// Immutable window state
|
||||
@ -293,9 +293,10 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
|
||||
this._terminalToolCreator = _instantiationService.createInstance(ToolTerminalCreator);
|
||||
this._commandSimplifier = _instantiationService.createInstance(CommandSimplifier, this._osBackend);
|
||||
this._profileFetcher = _instantiationService.createInstance(TerminalProfileFetcher);
|
||||
this._treeSitterCommandParser = this._instantiationService.createInstance(TreeSitterCommandParser);
|
||||
this._telemetry = _instantiationService.createInstance(RunInTerminalToolTelemetry);
|
||||
this._commandLineAutoApprover = this._register(_instantiationService.createInstance(CommandLineAutoApprover));
|
||||
this._profileFetcher = _instantiationService.createInstance(TerminalProfileFetcher);
|
||||
|
||||
// Clear out warning accepted state if the setting is disabled
|
||||
this._register(Event.runAndSubscribe(this._configurationService.onDidChangeConfiguration, e => {
|
||||
@ -349,94 +350,108 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
// can be reviewed in the terminal channel. It also allows gauging the effective set of
|
||||
// commands that would be auto approved if it were enabled.
|
||||
const actualCommand = toolEditedCommand ?? args.command;
|
||||
const subCommands = splitCommandLineIntoSubCommands(actualCommand, shell, os);
|
||||
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(actualCommand);
|
||||
const autoApproveReasons: string[] = [
|
||||
...subCommandResults.map(e => e.reason),
|
||||
commandLineResult.reason,
|
||||
];
|
||||
|
||||
let isAutoApproved = false;
|
||||
let isDenied = false;
|
||||
let autoApproveReason: 'subCommand' | 'commandLine' | undefined;
|
||||
let autoApproveDefault: boolean | undefined;
|
||||
let disclaimer: IMarkdownString | undefined;
|
||||
let customActions: ToolConfirmationAction[] | undefined;
|
||||
|
||||
const deniedSubCommandResult = subCommandResults.find(e => e.result === 'denied');
|
||||
if (deniedSubCommandResult) {
|
||||
this._logService.info('autoApprove: Sub-command DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = deniedSubCommandResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'subCommand';
|
||||
} else if (commandLineResult.result === 'denied') {
|
||||
this._logService.info('autoApprove: Command line DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'commandLine';
|
||||
} else {
|
||||
if (subCommandResults.every(e => e.result === 'approved')) {
|
||||
this._logService.info('autoApprove: All sub-commands auto-approved');
|
||||
autoApproveReason = 'subCommand';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = subCommandResults.every(e => e.rule?.isDefaultRule);
|
||||
} else {
|
||||
this._logService.info('autoApprove: All sub-commands NOT auto-approved');
|
||||
if (commandLineResult.result === 'approved') {
|
||||
this._logService.info('autoApprove: Command line auto-approved');
|
||||
autoApproveReason = 'commandLine';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
} else {
|
||||
this._logService.info('autoApprove: Command line NOT auto-approved');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log detailed auto approval reasoning
|
||||
for (const reason of autoApproveReasons) {
|
||||
this._logService.info(`- ${reason}`);
|
||||
}
|
||||
|
||||
// Apply auto approval or force it off depending on enablement/opt-in state
|
||||
const isAutoApproveEnabled = this._configurationService.getValue(TerminalChatAgentToolsSettingId.EnableAutoApprove) === true;
|
||||
const isAutoApproveWarningAccepted = this._storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false);
|
||||
const isAutoApproveAllowed = isAutoApproveEnabled && isAutoApproveWarningAccepted;
|
||||
if (isAutoApproveEnabled) {
|
||||
autoApproveInfo = this._createAutoApproveInfo(
|
||||
isAutoApproved,
|
||||
isDenied,
|
||||
let isAutoApproved = false;
|
||||
|
||||
let subCommands: string[] | undefined;
|
||||
const treeSitterLanguage = isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash;
|
||||
try {
|
||||
subCommands = await this._treeSitterCommandParser.extractSubCommands(treeSitterLanguage, actualCommand);
|
||||
this._logService.info(`RunInTerminalTool: autoApprove: Parsed sub-commands via ${treeSitterLanguage} grammar`, subCommands);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this._logService.info(`RunInTerminalTool: autoApprove: Failed to parse sub-commands via ${treeSitterLanguage} grammar`);
|
||||
}
|
||||
|
||||
if (subCommands) {
|
||||
const subCommandResults = subCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os));
|
||||
const commandLineResult = this._commandLineAutoApprover.isCommandLineAutoApproved(actualCommand);
|
||||
const autoApproveReasons: string[] = [
|
||||
...subCommandResults.map(e => e.reason),
|
||||
commandLineResult.reason,
|
||||
];
|
||||
|
||||
let isDenied = false;
|
||||
let autoApproveReason: 'subCommand' | 'commandLine' | undefined;
|
||||
let autoApproveDefault: boolean | undefined;
|
||||
|
||||
const deniedSubCommandResult = subCommandResults.find(e => e.result === 'denied');
|
||||
if (deniedSubCommandResult) {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Sub-command DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = deniedSubCommandResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'subCommand';
|
||||
} else if (commandLineResult.result === 'denied') {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Command line DENIED auto approval');
|
||||
isDenied = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
autoApproveReason = 'commandLine';
|
||||
} else {
|
||||
if (subCommandResults.every(e => e.result === 'approved')) {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: All sub-commands auto-approved');
|
||||
autoApproveReason = 'subCommand';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = subCommandResults.every(e => e.rule?.isDefaultRule);
|
||||
} else {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: All sub-commands NOT auto-approved');
|
||||
if (commandLineResult.result === 'approved') {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Command line auto-approved');
|
||||
autoApproveReason = 'commandLine';
|
||||
isAutoApproved = true;
|
||||
autoApproveDefault = commandLineResult.rule?.isDefaultRule;
|
||||
} else {
|
||||
this._logService.info('RunInTerminalTool: autoApprove: Command line NOT auto-approved');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log detailed auto approval reasoning
|
||||
for (const reason of autoApproveReasons) {
|
||||
this._logService.info(`RunInTerminalTool: autoApprove: - ${reason}`);
|
||||
}
|
||||
|
||||
// Apply auto approval or force it off depending on enablement/opt-in state
|
||||
if (isAutoApproveEnabled) {
|
||||
autoApproveInfo = this._createAutoApproveInfo(
|
||||
isAutoApproved,
|
||||
isDenied,
|
||||
autoApproveReason,
|
||||
subCommandResults,
|
||||
commandLineResult,
|
||||
);
|
||||
} else {
|
||||
isAutoApproved = false;
|
||||
}
|
||||
|
||||
// Send telemetry about auto approval process
|
||||
this._telemetry.logPrepare({
|
||||
terminalToolSessionId,
|
||||
subCommands,
|
||||
autoApproveAllowed: !isAutoApproveEnabled ? 'off' : isAutoApproveWarningAccepted ? 'allowed' : 'needsOptIn',
|
||||
autoApproveResult: isAutoApproved ? 'approved' : isDenied ? 'denied' : 'manual',
|
||||
autoApproveReason,
|
||||
subCommandResults,
|
||||
commandLineResult,
|
||||
);
|
||||
} else {
|
||||
isAutoApproved = false;
|
||||
}
|
||||
autoApproveDefault
|
||||
});
|
||||
|
||||
// Send telemetry about auto approval process
|
||||
this._telemetry.logPrepare({
|
||||
terminalToolSessionId,
|
||||
subCommands,
|
||||
autoApproveAllowed: !isAutoApproveEnabled ? 'off' : isAutoApproveWarningAccepted ? 'allowed' : 'needsOptIn',
|
||||
autoApproveResult: isAutoApproved ? 'approved' : isDenied ? 'denied' : 'manual',
|
||||
autoApproveReason,
|
||||
autoApproveDefault
|
||||
});
|
||||
// Add a disclaimer warning about prompt injection for common commands that return
|
||||
// content from the web
|
||||
const subCommandsLowerFirstWordOnly = subCommands.map(command => command.split(' ')[0].toLowerCase());
|
||||
if (!isAutoApproved && (
|
||||
subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLower.includes(command)) ||
|
||||
(isPowerShell(shell, os) && subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLowerPwshOnly.includes(command)))
|
||||
)) {
|
||||
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + localize('runInTerminal.promptInjectionDisclaimer', 'Web content may contain malicious code or attempt prompt injection attacks.'), { supportThemeIcons: true });
|
||||
}
|
||||
|
||||
// Add a disclaimer warning about prompt injection for common commands that return
|
||||
// content from the web
|
||||
let disclaimer: IMarkdownString | undefined;
|
||||
const subCommandsLowerFirstWordOnly = subCommands.map(command => command.split(' ')[0].toLowerCase());
|
||||
if (!isAutoApproved && (
|
||||
subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLower.includes(command)) ||
|
||||
(isPowerShell(shell, os) && subCommandsLowerFirstWordOnly.some(command => promptInjectionWarningCommandsLowerPwshOnly.includes(command)))
|
||||
)) {
|
||||
disclaimer = new MarkdownString(`$(${Codicon.info.id}) ` + localize('runInTerminal.promptInjectionDisclaimer', 'Web content may contain malicious code or attempt prompt injection attacks.'), { supportThemeIcons: true });
|
||||
}
|
||||
|
||||
let customActions: ToolConfirmationAction[] | undefined;
|
||||
if (!isAutoApproved && isAutoApproveEnabled) {
|
||||
customActions = generateAutoApproveActions(actualCommand, subCommands, { subCommandResults, commandLineResult });
|
||||
if (!isAutoApproved && isAutoApproveEnabled) {
|
||||
customActions = generateAutoApproveActions(actualCommand, subCommands, { subCommandResults, commandLineResult });
|
||||
}
|
||||
}
|
||||
|
||||
let shellType = basename(shell, '.exe');
|
||||
@ -985,7 +1000,7 @@ class BackgroundTerminalExecution extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalProfileFetcher {
|
||||
export class TerminalProfileFetcher {
|
||||
|
||||
readonly osBackend: Promise<OperatingSystem>;
|
||||
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { BugIndicatingError } from '../../../../../base/common/errors.js';
|
||||
import { derived, waitForState } from '../../../../../base/common/observable.js';
|
||||
import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
|
||||
import type { Language, Parser, Query } from '@vscode/tree-sitter-wasm';
|
||||
|
||||
export const enum TreeSitterCommandParserLanguage {
|
||||
Bash = 'bash',
|
||||
PowerShell = 'powershell',
|
||||
}
|
||||
|
||||
export class TreeSitterCommandParser {
|
||||
private readonly _parser: Promise<Parser>;
|
||||
private readonly _queries: Map<Language, Query> = new Map();
|
||||
|
||||
constructor(
|
||||
@ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService
|
||||
) {
|
||||
this._parser = this._treeSitterLibraryService.getParserClass().then(ParserCtor => new ParserCtor());
|
||||
}
|
||||
|
||||
async extractSubCommands(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise<string[]> {
|
||||
const parser = await this._parser;
|
||||
const language = await waitForState(derived(reader => {
|
||||
return this._treeSitterLibraryService.getLanguage(languageId, true, reader);
|
||||
}));
|
||||
parser.setLanguage(language);
|
||||
|
||||
const tree = parser.parse(commandLine);
|
||||
if (!tree) {
|
||||
throw new BugIndicatingError('Failed to parse tree');
|
||||
}
|
||||
|
||||
const query = await this._getQuery(language);
|
||||
if (!query) {
|
||||
throw new BugIndicatingError('Failed to create tree sitter query');
|
||||
}
|
||||
|
||||
const captures = query.captures(tree.rootNode);
|
||||
const subCommands = captures.map(e => e.node.text);
|
||||
return subCommands;
|
||||
}
|
||||
|
||||
private async _getQuery(language: Language): Promise<Query> {
|
||||
let query = this._queries.get(language);
|
||||
if (!query) {
|
||||
query = await this._treeSitterLibraryService.createQuery(language, '(command) @command');
|
||||
this._queries.set(language, query);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@ -257,24 +257,6 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Dangerous patterns
|
||||
//
|
||||
// Patterns that are considered dangerous as they may lead to inline command execution.
|
||||
// These will just get blocked outright to be on the safe side, at least until there's a
|
||||
// real parser https://github.com/microsoft/vscode/issues/261794
|
||||
|
||||
// `(command)` many shells execute commands inside parentheses
|
||||
'/\\(.+\\)/s': { approve: false, matchCommandLine: true },
|
||||
|
||||
// `{command}` many shells support execution inside curly braces, additionally this
|
||||
// typically means the sub-command detection system falls over currently
|
||||
'/\\{.+\\}/s': { approve: false, matchCommandLine: true },
|
||||
|
||||
// `\`command\`` many shells support execution inside backticks
|
||||
'/`.+`/s': { approve: false, matchCommandLine: true },
|
||||
|
||||
// endregion
|
||||
|
||||
// #region Dangerous commands
|
||||
//
|
||||
// There are countless dangerous commands available on the command line, the defaults
|
||||
|
||||
@ -1,469 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual } from 'assert';
|
||||
import { splitCommandLineIntoSubCommands } from '../../browser/subCommands.js';
|
||||
import { OperatingSystem } from '../../../../../../base/common/platform.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
|
||||
suite('splitCommandLineIntoSubCommands', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('should split command line into subcommands', () => {
|
||||
const commandLine = 'echo "Hello World" && ls -la || pwd';
|
||||
const expectedSubCommands = ['echo "Hello World"', 'ls -la', 'pwd'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
suite('bash/sh shell', () => {
|
||||
test('should split on logical operators', () => {
|
||||
const commandLine = 'echo test && ls -la || pwd';
|
||||
const expectedSubCommands = ['echo test', 'ls -la', 'pwd'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on pipes', () => {
|
||||
const commandLine = 'ls -la | grep test | wc -l';
|
||||
const expectedSubCommands = ['ls -la', 'grep test', 'wc -l'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on semicolons', () => {
|
||||
const commandLine = 'cd /tmp; ls -la; pwd';
|
||||
const expectedSubCommands = ['cd /tmp', 'ls -la', 'pwd'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on background operator', () => {
|
||||
const commandLine = 'sleep 5 & echo done';
|
||||
const expectedSubCommands = ['sleep 5', 'echo done'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on redirection operators', () => {
|
||||
const commandLine = 'echo test > output.txt && cat output.txt';
|
||||
const expectedSubCommands = ['echo test', 'output.txt', 'cat output.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on stderr redirection', () => {
|
||||
const commandLine = 'command 2> error.log && echo success';
|
||||
const expectedSubCommands = ['command', 'error.log', 'echo success'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on append redirection', () => {
|
||||
const commandLine = 'echo line1 >> file.txt && echo line2 >> file.txt';
|
||||
const expectedSubCommands = ['echo line1', 'file.txt', 'echo line2', 'file.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('zsh shell', () => {
|
||||
test('should split on zsh-specific operators', () => {
|
||||
const commandLine = 'echo test <<< "input" && ls';
|
||||
const expectedSubCommands = ['echo test', '"input"', 'ls'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on process substitution', () => {
|
||||
const commandLine = 'diff <(ls dir1) <(ls dir2)';
|
||||
const expectedSubCommands = ['diff', 'ls dir1)', 'ls dir2)'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on bidirectional redirection', () => {
|
||||
const commandLine = 'command <> file.txt && echo done';
|
||||
const expectedSubCommands = ['command', 'file.txt', 'echo done'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should handle complex zsh command chains', () => {
|
||||
const commandLine = 'ls | grep test && echo found || echo not found';
|
||||
const expectedSubCommands = ['ls', 'grep test', 'echo found', 'echo not found'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('PowerShell', () => {
|
||||
test('should not split on PowerShell logical operators', () => {
|
||||
const commandLine = 'Get-ChildItem -and Get-Location -or Write-Host "test"';
|
||||
const expectedSubCommands = ['Get-ChildItem -and Get-Location -or Write-Host "test"'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell pipes', () => {
|
||||
const commandLine = 'Get-Process | Where-Object Name -eq "notepad" | Stop-Process';
|
||||
const expectedSubCommands = ['Get-Process', 'Where-Object Name -eq "notepad"', 'Stop-Process'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell redirection', () => {
|
||||
const commandLine = 'Get-Process > processes.txt && Get-Content processes.txt';
|
||||
const expectedSubCommands = ['Get-Process', 'processes.txt', 'Get-Content processes.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('edge cases', () => {
|
||||
test('should return single command when no operators present', () => {
|
||||
const commandLine = 'echo "hello world"';
|
||||
const expectedSubCommands = ['echo "hello world"'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should handle empty command', () => {
|
||||
const commandLine = '';
|
||||
const expectedSubCommands: string[] = [];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should trim whitespace from subcommands', () => {
|
||||
const commandLine = 'echo test && ls -la || pwd';
|
||||
const expectedSubCommands = ['echo test', 'ls -la', 'pwd'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'sh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should handle multiple consecutive operators', () => {
|
||||
const commandLine = 'echo test && && ls';
|
||||
const expectedSubCommands = ['echo test', 'ls'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should handle unknown shell as sh', () => {
|
||||
const commandLine = 'echo test && ls -la';
|
||||
const expectedSubCommands = ['echo test', 'ls -la'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'unknown-shell', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('shell type detection', () => {
|
||||
test('should detect PowerShell variants', () => {
|
||||
const commandLine = 'Get-Process ; Get-Location';
|
||||
const expectedSubCommands = ['Get-Process', 'Get-Location'];
|
||||
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'powershell', OperatingSystem.Linux), expectedSubCommands);
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows), expectedSubCommands);
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'pwsh', OperatingSystem.Linux), expectedSubCommands);
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows), expectedSubCommands);
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'powershell-preview', OperatingSystem.Linux), expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should detect zsh specifically', () => {
|
||||
const commandLine = 'echo test <<< input';
|
||||
const expectedSubCommands = ['echo test', 'input'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should default to sh for other shells', () => {
|
||||
const commandLine = 'echo test && ls';
|
||||
const expectedSubCommands = ['echo test', 'ls'];
|
||||
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux), expectedSubCommands);
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'dash', OperatingSystem.Linux), expectedSubCommands);
|
||||
deepStrictEqual(splitCommandLineIntoSubCommands(commandLine, 'fish', OperatingSystem.Linux), expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('redirection tests', () => {
|
||||
suite('output redirection', () => {
|
||||
test('should split on basic output redirection', () => {
|
||||
const commandLine = 'echo hello > output.txt';
|
||||
const expectedSubCommands = ['echo hello', 'output.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on append redirection', () => {
|
||||
const commandLine = 'echo hello >> output.txt';
|
||||
const expectedSubCommands = ['echo hello', 'output.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on multiple output redirections', () => {
|
||||
const commandLine = 'ls > files.txt && cat files.txt > backup.txt';
|
||||
const expectedSubCommands = ['ls', 'files.txt', 'cat files.txt', 'backup.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on numbered file descriptor redirection', () => {
|
||||
const commandLine = 'command 1> stdout.txt 2> stderr.txt';
|
||||
const expectedSubCommands = ['command', 'stdout.txt', 'stderr.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on stderr-only redirection', () => {
|
||||
const commandLine = 'make 2> errors.log';
|
||||
const expectedSubCommands = ['make', 'errors.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on all output redirection (&>)', () => {
|
||||
const commandLine = 'command &> all_output.txt';
|
||||
const expectedSubCommands = ['command', 'all_output.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('input redirection', () => {
|
||||
test('should split on input redirection', () => {
|
||||
const commandLine = 'sort < input.txt';
|
||||
const expectedSubCommands = ['sort', 'input.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on numbered input redirection', () => {
|
||||
const commandLine = 'program 0< input.txt';
|
||||
const expectedSubCommands = ['program', 'input.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on input/output combined', () => {
|
||||
const commandLine = 'sort < input.txt > output.txt';
|
||||
const expectedSubCommands = ['sort', 'input.txt', 'output.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('stream redirection', () => {
|
||||
test('should split on stdout to stderr redirection', () => {
|
||||
const commandLine = 'echo error 1>&2';
|
||||
const expectedSubCommands = ['echo error'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on stderr to stdout redirection', () => {
|
||||
const commandLine = 'command 2>&1';
|
||||
const expectedSubCommands = ['command'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on stream redirection with numbered descriptors', () => {
|
||||
const commandLine = 'exec 3>&1 && exec 4>&2';
|
||||
const expectedSubCommands = ['exec', 'exec'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on multiple stream redirections', () => {
|
||||
const commandLine = 'command 2>&1 1>&3 3>&2';
|
||||
const expectedSubCommands = ['command'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('here documents and here strings', () => {
|
||||
test('should split on here document', () => {
|
||||
const commandLine = 'cat << EOF && echo done';
|
||||
const expectedSubCommands = ['cat', 'EOF', 'echo done'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on here string (bash/zsh)', () => {
|
||||
const commandLine = 'grep pattern <<< "search this text"';
|
||||
const expectedSubCommands = ['grep pattern', '"search this text"'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on numbered here string', () => {
|
||||
const commandLine = 'command 3<<< "input data"';
|
||||
const expectedSubCommands = ['command', '"input data"'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('bidirectional redirection', () => {
|
||||
test('should split on read/write redirection', () => {
|
||||
const commandLine = 'dialog <> /dev/tty1';
|
||||
const expectedSubCommands = ['dialog', '/dev/tty1'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on numbered bidirectional redirection', () => {
|
||||
const commandLine = 'program 3<> data.file';
|
||||
const expectedSubCommands = ['program', 'data.file'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('PowerShell redirection', () => {
|
||||
test('should split on PowerShell output redirection', () => {
|
||||
const commandLine = 'Get-Process > processes.txt';
|
||||
const expectedSubCommands = ['Get-Process', 'processes.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell append redirection', () => {
|
||||
const commandLine = 'Write-Output "log entry" >> log.txt';
|
||||
const expectedSubCommands = ['Write-Output "log entry"', 'log.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell error stream redirection', () => {
|
||||
const commandLine = 'Get-Content nonexistent.txt 2> errors.log';
|
||||
const expectedSubCommands = ['Get-Content nonexistent.txt', 'errors.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell warning stream redirection', () => {
|
||||
const commandLine = 'Get-Process 3> warnings.log';
|
||||
const expectedSubCommands = ['Get-Process', 'warnings.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell verbose stream redirection', () => {
|
||||
const commandLine = 'Get-ChildItem 4> verbose.log';
|
||||
const expectedSubCommands = ['Get-ChildItem', 'verbose.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell debug stream redirection', () => {
|
||||
const commandLine = 'Invoke-Command 5> debug.log';
|
||||
const expectedSubCommands = ['Invoke-Command', 'debug.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell information stream redirection', () => {
|
||||
const commandLine = 'Write-Information "info" 6> info.log';
|
||||
const expectedSubCommands = ['Write-Information "info"', 'info.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell all streams redirection', () => {
|
||||
const commandLine = 'Get-Process *> all_streams.log';
|
||||
const expectedSubCommands = ['Get-Process', 'all_streams.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'pwsh.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on PowerShell stream to stream redirection', () => {
|
||||
const commandLine = 'Write-Error "error" 2>&1';
|
||||
const expectedSubCommands = ['Write-Error "error"'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'powershell.exe', OperatingSystem.Windows);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('complex redirection scenarios', () => {
|
||||
test('should split on command with multiple redirections', () => {
|
||||
const commandLine = 'command < input.txt > output.txt 2> errors.log';
|
||||
const expectedSubCommands = ['command', 'input.txt', 'output.txt', 'errors.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on redirection with pipes and logical operators', () => {
|
||||
const commandLine = 'cat file.txt | grep pattern > results.txt && echo "Found" || echo "Not found" 2> errors.log';
|
||||
const expectedSubCommands = ['cat file.txt', 'grep pattern', 'results.txt', 'echo "Found"', 'echo "Not found"', 'errors.log'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on chained redirections', () => {
|
||||
const commandLine = 'echo "step1" > temp.txt && cat temp.txt >> final.txt && rm temp.txt';
|
||||
const expectedSubCommands = ['echo "step1"', 'temp.txt', 'cat temp.txt', 'final.txt', 'rm temp.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should handle redirection with background processes', () => {
|
||||
const commandLine = 'long_running_command > output.log 2>&1 & echo "started"';
|
||||
const expectedSubCommands = ['long_running_command', 'output.log', 'echo "started"'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
|
||||
suite('zsh-specific redirection', () => {
|
||||
test('should split on zsh noclobber override', () => {
|
||||
const commandLine = 'echo "force" >! existing_file.txt';
|
||||
const expectedSubCommands = ['echo "force"', 'existing_file.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on zsh clobber override', () => {
|
||||
const commandLine = 'echo "overwrite" >| protected_file.txt';
|
||||
const expectedSubCommands = ['echo "overwrite"', 'protected_file.txt'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on zsh process substitution for input', () => {
|
||||
const commandLine = 'diff <(sort file1.txt) <(sort file2.txt)';
|
||||
const expectedSubCommands = ['diff', 'sort file1.txt)', 'sort file2.txt)'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test('should split on zsh multios', () => {
|
||||
const commandLine = 'echo "test" | tee >(gzip > file1.gz) >(bzip2 > file1.bz2)';
|
||||
const expectedSubCommands = ['echo "test"', 'tee', '(gzip', 'file1.gz)', '(bzip2', 'file1.bz2)'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('complex command combinations', () => {
|
||||
test('should handle mixed operators in order', () => {
|
||||
const commandLine = 'ls | grep test && echo found > result.txt || echo failed';
|
||||
const expectedSubCommands = ['ls', 'grep test', 'echo found', 'result.txt', 'echo failed'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'bash', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
|
||||
test.skip('should handle subshells and braces', () => {
|
||||
const commandLine = '(cd /tmp && ls) && { echo done; }';
|
||||
const expectedSubCommands = ['(cd /tmp', 'ls)', '{ echo done', '}'];
|
||||
const actualSubCommands = splitCommandLineIntoSubCommands(commandLine, 'zsh', OperatingSystem.Linux);
|
||||
deepStrictEqual(actualSubCommands, expectedSubCommands);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -24,6 +24,17 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../../../
|
||||
import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js';
|
||||
import { count } from '../../../../../../base/common/strings.js';
|
||||
import { ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js';
|
||||
import { ITreeSitterLibraryService } from '../../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js';
|
||||
import { TreeSitterLibraryService } from '../../../../../services/treeSitter/browser/treeSitterLibraryService.js';
|
||||
import { FileService } from '../../../../../../platform/files/common/fileService.js';
|
||||
import { NullLogService } from '../../../../../../platform/log/common/log.js';
|
||||
import { IFileService } from '../../../../../../platform/files/common/files.js';
|
||||
import { Schemas } from '../../../../../../base/common/network.js';
|
||||
|
||||
// HACK: This test lives in electron-browser/ to ensure this node import works if the test is run in
|
||||
// web tests https://github.com/microsoft/vscode/issues/272777
|
||||
// eslint-disable-next-line local/code-layering, local/code-import-patterns
|
||||
import { DiskFileSystemProvider } from '../../../../../../platform/files/node/diskFileSystemProvider.js';
|
||||
|
||||
class TestRunInTerminalTool extends RunInTerminalTool {
|
||||
protected override _osBackend: Promise<OperatingSystem> = Promise.resolve(OperatingSystem.Windows);
|
||||
@ -42,6 +53,7 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
let configurationService: TestConfigurationService;
|
||||
let fileService: IFileService;
|
||||
let storageService: IStorageService;
|
||||
let terminalServiceDisposeEmitter: Emitter<ITerminalInstance>;
|
||||
let chatServiceDisposeEmitter: Emitter<{ sessionId: string; reason: 'cleared' }>;
|
||||
@ -50,13 +62,25 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
setup(() => {
|
||||
configurationService = new TestConfigurationService();
|
||||
|
||||
const logService = new NullLogService();
|
||||
fileService = store.add(new FileService(logService));
|
||||
const diskFileSystemProvider = store.add(new DiskFileSystemProvider(logService));
|
||||
store.add(fileService.registerProvider(Schemas.file, diskFileSystemProvider));
|
||||
|
||||
setConfig(TerminalChatAgentToolsSettingId.EnableAutoApprove, true);
|
||||
terminalServiceDisposeEmitter = new Emitter<ITerminalInstance>();
|
||||
chatServiceDisposeEmitter = new Emitter<{ sessionId: string; reason: 'cleared' }>();
|
||||
|
||||
instantiationService = workbenchInstantiationService({
|
||||
configurationService: () => configurationService,
|
||||
fileService: () => fileService,
|
||||
}, store);
|
||||
|
||||
const treeSitterLibraryService = store.add(instantiationService.createInstance(TreeSitterLibraryService));
|
||||
treeSitterLibraryService.isTest = true;
|
||||
instantiationService.stub(ITreeSitterLibraryService, treeSitterLibraryService);
|
||||
|
||||
instantiationService.stub(ILanguageModelToolsService, {
|
||||
getTools() {
|
||||
return [];
|
||||
@ -69,7 +93,7 @@ suite('RunInTerminalTool', () => {
|
||||
onDidDisposeSession: chatServiceDisposeEmitter.event
|
||||
});
|
||||
instantiationService.stub(ITerminalProfileResolverService, {
|
||||
getDefaultProfile: async () => ({ path: 'pwsh' } as ITerminalProfile)
|
||||
getDefaultProfile: async () => ({ path: 'bash' } as ITerminalProfile)
|
||||
});
|
||||
|
||||
storageService = instantiationService.get(IStorageService);
|
||||
@ -268,32 +292,19 @@ suite('RunInTerminalTool', () => {
|
||||
'HTTP_PROXY=proxy:8080 wget https://example.com',
|
||||
'VAR1=value1 VAR2=value2 echo test',
|
||||
'A=1 B=2 C=3 ./script.sh',
|
||||
|
||||
// Dangerous patterns
|
||||
'echo $(whoami)',
|
||||
'ls $(pwd)',
|
||||
'echo `date`',
|
||||
'cat `which ls`',
|
||||
'echo ${HOME}',
|
||||
'ls {a,b,c}',
|
||||
'echo (Get-Date)',
|
||||
|
||||
// Dangerous patterns - multi-line
|
||||
'echo "{\n}"',
|
||||
'echo @"\n{\n}"@',
|
||||
];
|
||||
|
||||
suite('auto approved', () => {
|
||||
for (const command of autoApprovedTestCases) {
|
||||
test(command.replaceAll('\n', '\\n'), async () => {
|
||||
assertAutoApproved(await executeToolTest({ command: command }));
|
||||
assertAutoApproved(await executeToolTest({ command }));
|
||||
});
|
||||
}
|
||||
});
|
||||
suite('confirmation required', () => {
|
||||
for (const command of confirmationRequiredTestCases) {
|
||||
test(command.replaceAll('\n', '\\n'), async () => {
|
||||
assertConfirmationRequired(await executeToolTest({ command: command }));
|
||||
assertConfirmationRequired(await executeToolTest({ command }));
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -319,7 +330,7 @@ suite('RunInTerminalTool', () => {
|
||||
command: 'rm file.txt',
|
||||
explanation: 'Remove a file'
|
||||
});
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
});
|
||||
|
||||
test('should require confirmation for commands in deny list even if in allow list', async () => {
|
||||
@ -332,7 +343,7 @@ suite('RunInTerminalTool', () => {
|
||||
command: 'rm dangerous-file.txt',
|
||||
explanation: 'Remove a dangerous file'
|
||||
});
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
});
|
||||
|
||||
test('should handle background commands with confirmation', async () => {
|
||||
@ -345,7 +356,7 @@ suite('RunInTerminalTool', () => {
|
||||
explanation: 'Start watching for file changes',
|
||||
isBackground: true
|
||||
});
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command? (background terminal)');
|
||||
assertConfirmationRequired(result, 'Run `bash` command? (background terminal)');
|
||||
});
|
||||
|
||||
test('should auto-approve background commands in allow list', async () => {
|
||||
@ -422,18 +433,6 @@ suite('RunInTerminalTool', () => {
|
||||
assertAutoApproved(result);
|
||||
});
|
||||
|
||||
test('should handle commands with only whitespace', async () => {
|
||||
setAutoApprove({
|
||||
echo: true
|
||||
});
|
||||
|
||||
const result = await executeToolTest({
|
||||
command: ' \t\n ',
|
||||
explanation: 'Whitespace only command'
|
||||
});
|
||||
assertConfirmationRequired(result);
|
||||
});
|
||||
|
||||
test('should handle matchCommandLine: true patterns', async () => {
|
||||
setAutoApprove({
|
||||
'/dangerous/': { approve: false, matchCommandLine: true },
|
||||
@ -504,7 +503,7 @@ suite('RunInTerminalTool', () => {
|
||||
explanation: 'Build the project'
|
||||
});
|
||||
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'npm run build' },
|
||||
'commandLine',
|
||||
@ -548,7 +547,7 @@ suite('RunInTerminalTool', () => {
|
||||
explanation: 'Build the project'
|
||||
});
|
||||
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
'configure',
|
||||
]);
|
||||
@ -560,7 +559,7 @@ suite('RunInTerminalTool', () => {
|
||||
explanation: 'Install dependencies and build'
|
||||
});
|
||||
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['npm install', 'npm run build'] },
|
||||
'commandLine',
|
||||
@ -578,7 +577,7 @@ suite('RunInTerminalTool', () => {
|
||||
explanation: 'Run foo command and show first 20 lines'
|
||||
});
|
||||
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: 'foo' },
|
||||
'commandLine',
|
||||
@ -610,7 +609,7 @@ suite('RunInTerminalTool', () => {
|
||||
explanation: 'Run multiple piped commands'
|
||||
});
|
||||
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
assertDropdownActions(result, [
|
||||
{ subCommand: ['foo', 'bar'] },
|
||||
'commandLine',
|
||||
@ -921,7 +920,7 @@ suite('RunInTerminalTool', () => {
|
||||
|
||||
clearAutoApproveWarningAcceptedState();
|
||||
|
||||
assertConfirmationRequired(await executeToolTest({ command: 'echo hello world' }), 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(await executeToolTest({ command: 'echo hello world' }), 'Run `bash` command?');
|
||||
});
|
||||
|
||||
test('should auto-approve commands when both auto-approve enabled and warning accepted', async () => {
|
||||
@ -940,7 +939,7 @@ suite('RunInTerminalTool', () => {
|
||||
});
|
||||
|
||||
const result = await executeToolTest({ command: 'echo hello world' });
|
||||
assertConfirmationRequired(result, 'Run `pwsh` command?');
|
||||
assertConfirmationRequired(result, 'Run `bash` command?');
|
||||
});
|
||||
});
|
||||
|
||||
@ -960,66 +959,25 @@ suite('RunInTerminalTool', () => {
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
suite('TerminalProfileFetcher', () => {
|
||||
suite('getCopilotProfile', () => {
|
||||
(isWindows ? test : test.skip)('should return custom profile when configured', async () => {
|
||||
runInTerminalTool.setBackendOs(OperatingSystem.Windows);
|
||||
const customProfile = Object.freeze({ path: 'C:\\Windows\\System32\\powershell.exe', args: ['-NoProfile'] });
|
||||
setConfig(TerminalChatAgentToolsSettingId.TerminalProfileWindows, customProfile);
|
||||
|
||||
suite('TerminalProfileFetcher', () => {
|
||||
const store = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
const result = await runInTerminalTool.profileFetcher.getCopilotProfile();
|
||||
strictEqual(result, customProfile);
|
||||
});
|
||||
|
||||
let instantiationService: TestInstantiationService;
|
||||
let configurationService: TestConfigurationService;
|
||||
let testTool: TestRunInTerminalTool;
|
||||
(isLinux ? test : test.skip)('should fall back to default shell when no custom profile is configured', async () => {
|
||||
runInTerminalTool.setBackendOs(OperatingSystem.Linux);
|
||||
setConfig(TerminalChatAgentToolsSettingId.TerminalProfileLinux, null);
|
||||
|
||||
setup(() => {
|
||||
configurationService = new TestConfigurationService();
|
||||
|
||||
instantiationService = workbenchInstantiationService({
|
||||
configurationService: () => configurationService,
|
||||
}, store);
|
||||
instantiationService.stub(ILanguageModelToolsService, {
|
||||
getTools() {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
instantiationService.stub(ITerminalService, {
|
||||
onDidDisposeInstance: new Emitter<ITerminalInstance>().event
|
||||
});
|
||||
instantiationService.stub(IChatService, {
|
||||
onDidDisposeSession: new Emitter<{ sessionId: string; reason: 'cleared' }>().event
|
||||
});
|
||||
instantiationService.stub(ITerminalProfileResolverService, {
|
||||
getDefaultProfile: async () => ({ path: 'pwsh' } as ITerminalProfile)
|
||||
});
|
||||
|
||||
testTool = store.add(instantiationService.createInstance(TestRunInTerminalTool));
|
||||
});
|
||||
|
||||
function setConfig(key: string, value: unknown) {
|
||||
configurationService.setUserConfiguration(key, value);
|
||||
configurationService.onDidChangeConfigurationEmitter.fire({
|
||||
affectsConfiguration: () => true,
|
||||
affectedKeys: new Set([key]),
|
||||
source: ConfigurationTarget.USER,
|
||||
change: null!,
|
||||
});
|
||||
}
|
||||
|
||||
suite('getCopilotProfile', () => {
|
||||
(isWindows ? test : test.skip)('should return custom profile when configured', async () => {
|
||||
testTool.setBackendOs(OperatingSystem.Windows);
|
||||
const customProfile = Object.freeze({ path: 'C:\\Windows\\System32\\powershell.exe', args: ['-NoProfile'] });
|
||||
setConfig(TerminalChatAgentToolsSettingId.TerminalProfileWindows, customProfile);
|
||||
|
||||
const result = await testTool.profileFetcher.getCopilotProfile();
|
||||
strictEqual(result, customProfile);
|
||||
});
|
||||
|
||||
(isLinux ? test : test.skip)('should fall back to default shell when no custom profile is configured', async () => {
|
||||
testTool.setBackendOs(OperatingSystem.Linux);
|
||||
setConfig(TerminalChatAgentToolsSettingId.TerminalProfileLinux, null);
|
||||
|
||||
const result = await testTool.profileFetcher.getCopilotProfile();
|
||||
strictEqual(typeof result, 'object');
|
||||
strictEqual((result as ITerminalProfile).path, 'pwsh'); // From the mock ITerminalProfileResolverService
|
||||
const result = await runInTerminalTool.profileFetcher.getCopilotProfile();
|
||||
strictEqual(typeof result, 'object');
|
||||
strictEqual((result as ITerminalProfile).path, 'bash');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -121,8 +121,8 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL
|
||||
return treeSitter.Parser;
|
||||
}
|
||||
|
||||
getLanguage(languageId: string, reader: IReader | undefined): Language | undefined {
|
||||
if (!this.supportsLanguage(languageId, reader)) {
|
||||
getLanguage(languageId: string, ignoreSupportsCheck: boolean, reader: IReader | undefined): Language | undefined {
|
||||
if (!ignoreSupportsCheck && !this.supportsLanguage(languageId, reader)) {
|
||||
return undefined;
|
||||
}
|
||||
const lang = this._languagesCache.get(languageId).resolvedValue.read(reader);
|
||||
@ -144,6 +144,11 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL
|
||||
const query = this._injectionQueries.get({ languageId, kind: 'highlights' }).read(reader);
|
||||
return query;
|
||||
}
|
||||
|
||||
async createQuery(language: Language, querySource: string): Promise<Query> {
|
||||
const treeSitter = await this._treeSitterImport.value;
|
||||
return new treeSitter.Query(language, querySource);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryReadFile(fileService: IFileService, uri: URI): Promise<IFileContent | undefined> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user