From 5f4c61a11532a0c92d1b35aa016dbb9402d887b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 20 Oct 2025 07:25:25 -0700 Subject: [PATCH 01/20] Initial working tree-sitter for command parsing Part of #261794 --- .../treeSitter/treeSitterLibraryService.ts | 2 ++ .../browser/commandLineAutoApprover.ts | 25 +++++++++++++++++++ .../browser/treeSitterLibraryService.ts | 15 +++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts index b24a348c2d4..e4588e6348d 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts @@ -28,4 +28,6 @@ export interface ITreeSitterLibraryService { * @param reader */ getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined; + + createQuery(languageId: string, reader: IReader | undefined, querySource: string): Promise; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index 291d53028c9..f660ef9dd84 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -11,6 +11,8 @@ import { structuralEquals } from '../../../../../base/common/equals.js'; import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; import { isPowerShell } from './runInTerminalHelpers.js'; +import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; +import { timeout } from '../../../../../base/common/async.js'; export interface IAutoApproveRule { regex: RegExp; @@ -39,6 +41,7 @@ export class CommandLineAutoApprover extends Disposable { constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, + @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService ) { super(); this.updateConfiguration(); @@ -50,6 +53,28 @@ export class CommandLineAutoApprover extends Disposable { this.updateConfiguration(); } })); + + const parserClass = this._treeSitterLibraryService.getParserClass(); + parserClass.then(async parserCtor => { + // HACK: Trigger async load + _treeSitterLibraryService.getLanguage('bash', undefined); + await timeout(1000); + const lang = _treeSitterLibraryService.getLanguage('bash', undefined); + + const parser = new parserCtor(); + if (lang) { + parser.setLanguage(lang); + const tree = parser.parse('echo "$(evil) a|b|c" | ls'); + + const q = await _treeSitterLibraryService.createQuery('bash', undefined, '(command) @command'); + if (tree && q) { + const captures = q.captures(tree.rootNode); + const subCommands = captures.map(e => e.node.text); + console.log('done', subCommands); + } + } + }); + } updateConfiguration() { diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts index 6302e6c322c..c641fd48dce 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts @@ -144,6 +144,21 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL const query = this._injectionQueries.get({ languageId, kind: 'highlights' }).read(reader); return query; } + + async createQuery(languageId: string, reader: IReader | undefined, querySource: string): Promise { + if (!this.supportsLanguage(languageId, reader)) { + return undefined; + } + const [ + language, + treeSitter + ] = await Promise.all([ + this._languagesCache.get(languageId).promise, + this._treeSitterImport.value, + ]); + + return new treeSitter.Query(language, querySource); + } } async function tryReadFile(fileService: IFileService, uri: URI): Promise { From 5bb3b395939d7b2becd83b5256a0a2342f00379c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 20 Oct 2025 07:46:36 -0700 Subject: [PATCH 02/20] Get powershell working --- .../browser/commandLineAutoApprover.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index f660ef9dd84..1a8af7f8d7d 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -58,12 +58,14 @@ export class CommandLineAutoApprover extends Disposable { parserClass.then(async parserCtor => { // HACK: Trigger async load _treeSitterLibraryService.getLanguage('bash', undefined); + _treeSitterLibraryService.getLanguage('powershell', undefined); await timeout(1000); - const lang = _treeSitterLibraryService.getLanguage('bash', undefined); + const bashLang = _treeSitterLibraryService.getLanguage('bash', undefined); + const pwshLang = _treeSitterLibraryService.getLanguage('powershell', undefined); const parser = new parserCtor(); - if (lang) { - parser.setLanguage(lang); + if (bashLang) { + parser.setLanguage(bashLang); const tree = parser.parse('echo "$(evil) a|b|c" | ls'); const q = await _treeSitterLibraryService.createQuery('bash', undefined, '(command) @command'); @@ -73,6 +75,18 @@ export class CommandLineAutoApprover extends Disposable { console.log('done', subCommands); } } + + if (pwshLang) { + parser.setLanguage(pwshLang); + const tree = parser.parse('Get-ChildItem | Write-Host "$(evil)"'); + + const q = await _treeSitterLibraryService.createQuery('powershell', undefined, '(command\ncommand_name: (command_name) @function)'); + if (tree && q) { + const captures = q.captures(tree.rootNode); + const subCommands = captures.map(e => e.node.text); + console.log('done', subCommands); + } + } }); } From ca9fc427e2159c59f01171686e32112baeebf144 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:21:11 -0700 Subject: [PATCH 03/20] Add Sync suffix and jsdoc to tree sitter service --- .../treeSitterSyntaxTokenBackend.ts | 6 +- .../treeSitter/treeSitterLibraryService.ts | 61 ++++++++++++++----- .../standaloneTreeSitterLibraryService.ts | 6 +- .../services/testTreeSitterLibraryService.ts | 10 ++- .../browser/commandLineAutoApprover.ts | 12 ++-- .../browser/treeSitterLibraryService.ts | 8 +-- 6 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts b/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts index 5d61d664043..d322f530088 100644 --- a/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts +++ b/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts @@ -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.getLanguageSync(currentLanguage, reader); if (!treeSitterLang) { return undefined; } @@ -65,7 +65,7 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend { })); parser.setLanguage(treeSitterLang); - const queries = this._treeSitterLibraryService.getInjectionQueries(currentLanguage, reader); + const queries = this._treeSitterLibraryService.getInjectionQueriesSync(currentLanguage, reader); if (queries === undefined) { return undefined; } @@ -80,7 +80,7 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend { return undefined; } - const queries = this._treeSitterLibraryService.getHighlightingQueries(treeModel.languageId, reader); + const queries = this._treeSitterLibraryService.getHighlightingQueriesSync(treeModel.languageId, reader); if (!queries) { return undefined; } diff --git a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts index e4588e6348d..003ee708683 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts @@ -12,22 +12,55 @@ export const ITreeSitterLibraryService = createDecorator; + /** + * 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; - 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 - */ - 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 - */ - getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined; - createQuery(languageId: string, reader: IReader | undefined, querySource: string): Promise; + /** + * Gets the Tree-sitter Language object synchronously. + * + * Note that This method runs synchronously and may fail if the language is + * not yet cached, as synchronous methods are required by editor APIs. + * @param languageId The language identifier to retrieve. + * @param reader Optional observable reader. + */ + getLanguageSync(languageId: string, 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. + * + * Note that This method runs synchronously and may fail if the language is + * not yet cached, as synchronous methods are required by editor APIs. + * @param languageId The language identifier to retrieve queries for. + * @param reader Optional observable reader. + */ + getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined; + + /** + * Gets the highlighting queries for a language. A return value of `null` + * indicates that there are no highlights queries for this language. + * + * Note that This method runs synchronously and may fail if the language is + * not yet cached, as synchronous methods are required by editor APIs. + * @param languageId The language identifier to retrieve queries for. + * @param reader Optional observable reader. + */ + getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined; + + /** + * Creates a custom query for a language. Returns undefiend if + * @param languageId The language identifier to create the query for. + * @param reader Optional observable reader. + * @param querySource The query source string to compile. + */ + createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise; } diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts index f5e4f3f95f4..121d877cbd6 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts @@ -18,7 +18,7 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer return false; } - getLanguage(languageId: string, reader: IReader | undefined): Language | undefined { + getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { return undefined; } /** @@ -26,7 +26,7 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer * @param languageId * @param reader */ - getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { + getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } /** @@ -34,7 +34,7 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer * @param languageId * @param reader */ - getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { + getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } } diff --git a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts index e07e75740a9..b5416d9cfad 100644 --- a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts @@ -18,15 +18,19 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService { return false; } - getLanguage(languageId: string, reader: IReader | undefined): Language | undefined { + getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { return undefined; } - getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { + getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } - getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { + getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } + + async createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise { + return undefined; + } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index 1a8af7f8d7d..89c6aad84c7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -57,18 +57,18 @@ export class CommandLineAutoApprover extends Disposable { const parserClass = this._treeSitterLibraryService.getParserClass(); parserClass.then(async parserCtor => { // HACK: Trigger async load - _treeSitterLibraryService.getLanguage('bash', undefined); - _treeSitterLibraryService.getLanguage('powershell', undefined); + _treeSitterLibraryService.getLanguageSync('bash', undefined); + _treeSitterLibraryService.getLanguageSync('powershell', undefined); await timeout(1000); - const bashLang = _treeSitterLibraryService.getLanguage('bash', undefined); - const pwshLang = _treeSitterLibraryService.getLanguage('powershell', undefined); + const bashLang = _treeSitterLibraryService.getLanguageSync('bash', undefined); + const pwshLang = _treeSitterLibraryService.getLanguageSync('powershell', undefined); const parser = new parserCtor(); if (bashLang) { parser.setLanguage(bashLang); const tree = parser.parse('echo "$(evil) a|b|c" | ls'); - const q = await _treeSitterLibraryService.createQuery('bash', undefined, '(command) @command'); + const q = await _treeSitterLibraryService.createQuery('bash', '(command) @command', undefined); if (tree && q) { const captures = q.captures(tree.rootNode); const subCommands = captures.map(e => e.node.text); @@ -80,7 +80,7 @@ export class CommandLineAutoApprover extends Disposable { parser.setLanguage(pwshLang); const tree = parser.parse('Get-ChildItem | Write-Host "$(evil)"'); - const q = await _treeSitterLibraryService.createQuery('powershell', undefined, '(command\ncommand_name: (command_name) @function)'); + const q = await _treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)', undefined); if (tree && q) { const captures = q.captures(tree.rootNode); const subCommands = captures.map(e => e.node.text); diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts index c641fd48dce..1fafa751551 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts @@ -121,7 +121,7 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return treeSitter.Parser; } - getLanguage(languageId: string, reader: IReader | undefined): Language | undefined { + getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { if (!this.supportsLanguage(languageId, reader)) { return undefined; } @@ -129,7 +129,7 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return lang; } - getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { + getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { if (!this.supportsLanguage(languageId, reader)) { return undefined; } @@ -137,7 +137,7 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return query; } - getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { + getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { if (!this.supportsLanguage(languageId, reader)) { return undefined; } @@ -145,7 +145,7 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return query; } - async createQuery(languageId: string, reader: IReader | undefined, querySource: string): Promise { + async createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise { if (!this.supportsLanguage(languageId, reader)) { return undefined; } From f11c1f3e26f075be81bf8a70e031b7a0319d3665 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:26:46 -0700 Subject: [PATCH 04/20] Add ITreeSitterLibraryService.getLanguage async api --- .../treeSitter/treeSitterLibraryService.ts | 7 ++++++ .../standaloneTreeSitterLibraryService.ts | 22 +++++++++---------- .../services/testTreeSitterLibraryService.ts | 6 ++++- .../browser/treeSitterLibraryService.ts | 4 ++++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts index 003ee708683..78146ef0dca 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts @@ -24,6 +24,13 @@ export interface ITreeSitterLibraryService { */ supportsLanguage(languageId: string, reader: IReader | undefined): boolean; + /** + * Gets the Tree-sitter Language object. + * @param languageId The language identifier to retrieve. + * @param reader Optional observable reader. + */ + getLanguage(languageId: string): Promise; + /** * Gets the Tree-sitter Language object synchronously. * diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts index 121d877cbd6..837ef46ce7a 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts @@ -11,30 +11,30 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer readonly _serviceBrand: undefined; getParserClass(): Promise { - 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; } + async getLanguage(languageId: string): Promise { + throw new Error('not implemented in TestTreeSitterLibraryService'); + } + getLanguageSync(languageId: string, 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 - */ + getInjectionQueriesSync(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 - */ + getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } + + async createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise { + return undefined; + } } diff --git a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts index b5416d9cfad..f90d98ee7f4 100644 --- a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts @@ -11,13 +11,17 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService { readonly _serviceBrand: undefined; getParserClass(): Promise { - 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; } + async getLanguage(languageId: string): Promise { + throw new Error('not implemented in TestTreeSitterLibraryService'); + } + getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { return undefined; } diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts index 1fafa751551..07be55eaf06 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts @@ -121,6 +121,10 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return treeSitter.Parser; } + async getLanguage(languageId: string): Promise { + return this._languagesCache.get(languageId).promise; + } + getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { if (!this.supportsLanguage(languageId, reader)) { return undefined; From d9fe576718e824fd1f2da4ccd0422a2b344edef6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:32:55 -0700 Subject: [PATCH 05/20] Use async API in test code --- .../browser/commandLineAutoApprover.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index 89c6aad84c7..79c105eb84e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -12,7 +12,6 @@ import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; import { isPowerShell } from './runInTerminalHelpers.js'; import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; -import { timeout } from '../../../../../base/common/async.js'; export interface IAutoApproveRule { regex: RegExp; @@ -56,19 +55,15 @@ export class CommandLineAutoApprover extends Disposable { const parserClass = this._treeSitterLibraryService.getParserClass(); parserClass.then(async parserCtor => { - // HACK: Trigger async load - _treeSitterLibraryService.getLanguageSync('bash', undefined); - _treeSitterLibraryService.getLanguageSync('powershell', undefined); - await timeout(1000); - const bashLang = _treeSitterLibraryService.getLanguageSync('bash', undefined); - const pwshLang = _treeSitterLibraryService.getLanguageSync('powershell', undefined); + const bashLang = await this._treeSitterLibraryService.getLanguage('bash'); + const pwshLang = await this._treeSitterLibraryService.getLanguage('powershell'); const parser = new parserCtor(); if (bashLang) { parser.setLanguage(bashLang); const tree = parser.parse('echo "$(evil) a|b|c" | ls'); - const q = await _treeSitterLibraryService.createQuery('bash', '(command) @command', undefined); + const q = await this._treeSitterLibraryService.createQuery('bash', '(command) @command', undefined); if (tree && q) { const captures = q.captures(tree.rootNode); const subCommands = captures.map(e => e.node.text); @@ -80,7 +75,7 @@ export class CommandLineAutoApprover extends Disposable { parser.setLanguage(pwshLang); const tree = parser.parse('Get-ChildItem | Write-Host "$(evil)"'); - const q = await _treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)', undefined); + const q = await this._treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)', undefined); if (tree && q) { const captures = q.captures(tree.rootNode); const subCommands = captures.map(e => e.node.text); From 9013d2a9ecc09ee2d0e6885e14eca29d3c09c7d8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Oct 2025 08:04:32 -0700 Subject: [PATCH 06/20] Update tree sitter, fix createQuery api --- package-lock.json | 8 ++++---- package.json | 2 +- .../services/treeSitter/treeSitterLibraryService.ts | 2 +- .../browser/standaloneTreeSitterLibraryService.ts | 6 +++--- .../test/common/services/testTreeSitterLibraryService.ts | 4 ++-- .../chatAgentTools/browser/commandLineAutoApprover.ts | 4 ++-- .../treeSitter/browser/treeSitterLibraryService.ts | 5 +---- 7 files changed, 14 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index e322ffc91d9..879bca4ff26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index aa84a3b0265..f2adbeeea1a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts index 78146ef0dca..096cb8ded88 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts @@ -69,5 +69,5 @@ export interface ITreeSitterLibraryService { * @param reader Optional observable reader. * @param querySource The query source string to compile. */ - createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise; + createQuery(languageId: string, querySource: string): Promise; } diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts index 837ef46ce7a..123cb671ee4 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts @@ -19,7 +19,7 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer } async getLanguage(languageId: string): Promise { - throw new Error('not implemented in TestTreeSitterLibraryService'); + throw new Error('not implemented in StandaloneTreeSitterLibraryService'); } getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { @@ -34,7 +34,7 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer return null; } - async createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise { - return undefined; + async createQuery(languageId: string, querySource: string): Promise { + throw new Error('not implemented in StandaloneTreeSitterLibraryService'); } } diff --git a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts index f90d98ee7f4..d90cfd0b67b 100644 --- a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts @@ -34,7 +34,7 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService { return null; } - async createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise { - return undefined; + async createQuery(languageId: string, querySource: string): Promise { + throw new Error('not implemented in TestTreeSitterLibraryService'); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index 79c105eb84e..73a83f5cec4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -63,7 +63,7 @@ export class CommandLineAutoApprover extends Disposable { parser.setLanguage(bashLang); const tree = parser.parse('echo "$(evil) a|b|c" | ls'); - const q = await this._treeSitterLibraryService.createQuery('bash', '(command) @command', undefined); + const q = await this._treeSitterLibraryService.createQuery('bash', '(command) @command'); if (tree && q) { const captures = q.captures(tree.rootNode); const subCommands = captures.map(e => e.node.text); @@ -75,7 +75,7 @@ export class CommandLineAutoApprover extends Disposable { parser.setLanguage(pwshLang); const tree = parser.parse('Get-ChildItem | Write-Host "$(evil)"'); - const q = await this._treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)', undefined); + const q = await this._treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)'); if (tree && q) { const captures = q.captures(tree.rootNode); const subCommands = captures.map(e => e.node.text); diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts index 07be55eaf06..82ef2193d31 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts @@ -149,10 +149,7 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return query; } - async createQuery(languageId: string, querySource: string, reader: IReader | undefined): Promise { - if (!this.supportsLanguage(languageId, reader)) { - return undefined; - } + async createQuery(languageId: string, querySource: string): Promise { const [ language, treeSitter From f4edae643bb92d505f77d22c43b411514ba67677 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:01:44 -0700 Subject: [PATCH 07/20] Move tree sitter parsing into own class --- eslint.config.js | 1 + .../browser/commandLineAutoApprover.ts | 34 -- .../chatAgentTools/browser/subCommands.ts | 75 --- .../browser/tools/runInTerminalTool.ts | 25 +- .../browser/treeSitterCommandParser.ts | 52 ++ .../test/browser/subCommands.test.ts | 469 ------------------ 6 files changed, 70 insertions(+), 586 deletions(-) delete mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts create mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts delete mode 100644 src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts diff --git a/eslint.config.js b/eslint.config.js index 509648a12c1..9fb6db94c50 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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' diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts index 73a83f5cec4..291d53028c9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts @@ -11,7 +11,6 @@ import { structuralEquals } from '../../../../../base/common/equals.js'; import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; import { isPowerShell } from './runInTerminalHelpers.js'; -import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; export interface IAutoApproveRule { regex: RegExp; @@ -40,7 +39,6 @@ export class CommandLineAutoApprover extends Disposable { constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService ) { super(); this.updateConfiguration(); @@ -52,38 +50,6 @@ export class CommandLineAutoApprover extends Disposable { this.updateConfiguration(); } })); - - const parserClass = this._treeSitterLibraryService.getParserClass(); - parserClass.then(async parserCtor => { - const bashLang = await this._treeSitterLibraryService.getLanguage('bash'); - const pwshLang = await this._treeSitterLibraryService.getLanguage('powershell'); - - const parser = new parserCtor(); - if (bashLang) { - parser.setLanguage(bashLang); - const tree = parser.parse('echo "$(evil) a|b|c" | ls'); - - const q = await this._treeSitterLibraryService.createQuery('bash', '(command) @command'); - if (tree && q) { - const captures = q.captures(tree.rootNode); - const subCommands = captures.map(e => e.node.text); - console.log('done', subCommands); - } - } - - if (pwshLang) { - parser.setLanguage(pwshLang); - const tree = parser.parse('Get-ChildItem | Write-Host "$(evil)"'); - - const q = await this._treeSitterLibraryService.createQuery('powershell', '(command\ncommand_name: (command_name) @function)'); - if (tree && q) { - const captures = q.captures(tree.rootNode); - const subCommands = captures.map(e => e.node.text); - console.log('done', subCommands); - } - } - }); - } updateConfiguration() { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts deleted file mode 100644 index df98f6c64f8..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/subCommands.ts +++ /dev/null @@ -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); -} 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 ab6bec13e81..59d79407f44 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -43,10 +43,10 @@ import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; import { dedupeRules, generateAutoApproveActions, isPowerShell } 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'; const enum TerminalToolStorageKeysInternal { TerminalSession = 'chat.terminalSessions' @@ -148,6 +148,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { private readonly _terminalToolCreator: ToolTerminalCreator; private readonly _commandSimplifier: CommandSimplifier; + private readonly _treeSitterCommandParser: TreeSitterCommandParser; private readonly _telemetry: RunInTerminalToolTelemetry; protected readonly _commandLineAutoApprover: CommandLineAutoApprover; protected readonly _sessionTerminalAssociations: Map = new Map(); @@ -182,6 +183,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { this._terminalToolCreator = _instantiationService.createInstance(ToolTerminalCreator); this._commandSimplifier = _instantiationService.createInstance(CommandSimplifier, this._osBackend); + this._treeSitterCommandParser = this._instantiationService.createInstance(TreeSitterCommandParser); this._telemetry = _instantiationService.createInstance(RunInTerminalToolTelemetry); this._commandLineAutoApprover = this._register(_instantiationService.createInstance(CommandLineAutoApprover)); @@ -237,7 +239,14 @@ 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 treeSitterLanguage = isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash; + // TODO: Handle throw + const subCommands = await this._treeSitterCommandParser.extractSubCommands(treeSitterLanguage, actualCommand); + this._logService.info('RunInTerminalTool: autoApprove: Parsed sub-commands', subCommands); + + + // 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[] = [ @@ -252,30 +261,30 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const deniedSubCommandResult = subCommandResults.find(e => e.result === 'denied'); if (deniedSubCommandResult) { - this._logService.info('autoApprove: Sub-command DENIED auto approval'); + 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('autoApprove: Command line DENIED auto approval'); + 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('autoApprove: All sub-commands auto-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('autoApprove: All sub-commands NOT auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: All sub-commands NOT auto-approved'); if (commandLineResult.result === 'approved') { - this._logService.info('autoApprove: Command line auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: Command line auto-approved'); autoApproveReason = 'commandLine'; isAutoApproved = true; autoApproveDefault = commandLineResult.rule?.isDefaultRule; } else { - this._logService.info('autoApprove: Command line NOT auto-approved'); + this._logService.info('RunInTerminalTool: autoApprove: Command line NOT auto-approved'); } } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts new file mode 100644 index 00000000000..b495aaa51e6 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Lazy } from '../../../../../base/common/lazy.js'; +import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; +import type { Parser, Query } from '@vscode/tree-sitter-wasm'; + +export const enum TreeSitterCommandParserLanguage { + Bash = 'bash', + PowerShell = 'powershell', +} + +export class TreeSitterCommandParser { + private readonly _parser: Promise; + + private readonly _languageToQueryMap = { + [TreeSitterCommandParserLanguage.Bash]: new Lazy(() => { + return this._treeSitterLibraryService.createQuery(TreeSitterCommandParserLanguage.Bash, '(command) @command'); + }), + [TreeSitterCommandParserLanguage.PowerShell]: new Lazy(() => { + return this._treeSitterLibraryService.createQuery(TreeSitterCommandParserLanguage.PowerShell, '(command\ncommand_name: (command_name) @function)'); + }), + } satisfies { [K in TreeSitterCommandParserLanguage]: Lazy> }; + + constructor( + @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService + ) { + this._parser = this._treeSitterLibraryService.getParserClass().then(ParserCtor => new ParserCtor()); + } + + async extractSubCommands(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { + const parser = await this._parser; + parser.setLanguage(await this._treeSitterLibraryService.getLanguage(languageId)); + + const tree = parser.parse(commandLine); + if (!tree) { + throw new BugIndicatingError('Failed to parse tree'); + } + + const query = await this._languageToQueryMap[languageId].value; + 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; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts deleted file mode 100644 index cf1d81fd1ca..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/subCommands.test.ts +++ /dev/null @@ -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); - }); - }); -}); From cfc2ee927188d44dfabade8bd2c4d6c862d6442e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:05:02 -0700 Subject: [PATCH 08/20] Try force ; chaining usage in all pwshs --- .../chatAgentTools/browser/tools/runInTerminalTool.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 ea5fac72309..0d92a893626 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -41,7 +41,7 @@ import type { ITerminalExecuteStrategy } from '../executeStrategy/executeStrateg import { NoneExecuteStrategy } from '../executeStrategy/noneExecuteStrategy.js'; import { RichExecuteStrategy } from '../executeStrategy/richExecuteStrategy.js'; import { getOutput } from '../outputHelpers.js'; -import { dedupeRules, generateAutoApproveActions, isFish, isPowerShell, isWindowsPowerShell, isZsh } from '../runInTerminalHelpers.js'; +import { dedupeRules, generateAutoApproveActions, isFish, isPowerShell, isZsh } from '../runInTerminalHelpers.js'; import { RunInTerminalToolTelemetry } from '../runInTerminalToolTelemetry.js'; import { ShellIntegrationQuality, ToolTerminalCreator, type IToolTerminal } from '../toolTerminalCreator.js'; import { OutputMonitor } from './monitoring/outputMonitor.js'; @@ -56,9 +56,9 @@ function createPowerShellModelDescription(shell: string): string { '', 'Command Execution:', '- Does NOT support multi-line commands', - `- ${isWindowsPowerShell(shell) - ? '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 `&&` + '- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly', '- Prefer pipelines | for object-based data flow', '', 'Directory Management:', From f35c9b56a41024f1702a8b7b8f484b905be05a0e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 05:57:56 -0700 Subject: [PATCH 09/20] Don't auto approve when parsing fails --- .../browser/tools/runInTerminalTool.ts | 178 +++++++++--------- 1 file changed, 93 insertions(+), 85 deletions(-) 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 edf82c8f656..ce6765a60da 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -353,99 +353,107 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { const actualCommand = toolEditedCommand ?? args.command; const treeSitterLanguage = isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash; - // TODO: Handle throw - const subCommands = await this._treeSitterCommandParser.extractSubCommands(treeSitterLanguage, actualCommand); - this._logService.info('RunInTerminalTool: autoApprove: Parsed sub-commands', subCommands); + let disclaimer: IMarkdownString | undefined; + let customActions: ToolConfirmationAction[] | undefined; - // 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; - - 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(`- ${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; + 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(`- ${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'); From 2fe79c9b7e7fee5cc4896097ca5122c7004d41dc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 06:00:23 -0700 Subject: [PATCH 10/20] Remove broad deny rules as tree sitter can parse now --- .../terminalChatAgentToolsConfiguration.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts index b0d475dd1c7..1b8384b92f2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts @@ -255,24 +255,6 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary Date: Wed, 22 Oct 2025 06:49:11 -0700 Subject: [PATCH 11/20] Revert some changes to TreeSitterLibraryService --- .../treeSitterSyntaxTokenBackend.ts | 6 ++-- .../treeSitter/treeSitterLibraryService.ts | 28 ++++--------------- .../standaloneTreeSitterLibraryService.ts | 10 ++----- .../services/testTreeSitterLibraryService.ts | 10 ++----- .../browser/tools/runInTerminalTool.ts | 3 +- .../browser/treeSitterCommandParser.ts | 6 +++- .../browser/treeSitterLibraryService.ts | 12 +++----- 7 files changed, 25 insertions(+), 50 deletions(-) diff --git a/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts b/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts index d322f530088..de7890799df 100644 --- a/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts +++ b/src/vs/editor/common/model/tokens/treeSitter/treeSitterSyntaxTokenBackend.ts @@ -54,7 +54,7 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend { } const currentLanguage = this._languageIdObs.read(reader); - const treeSitterLang = this._treeSitterLibraryService.getLanguageSync(currentLanguage, reader); + const treeSitterLang = this._treeSitterLibraryService.getLanguage(currentLanguage, false, reader); if (!treeSitterLang) { return undefined; } @@ -65,7 +65,7 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend { })); parser.setLanguage(treeSitterLang); - const queries = this._treeSitterLibraryService.getInjectionQueriesSync(currentLanguage, reader); + const queries = this._treeSitterLibraryService.getInjectionQueries(currentLanguage, reader); if (queries === undefined) { return undefined; } @@ -80,7 +80,7 @@ export class TreeSitterSyntaxTokenBackend extends AbstractSyntaxTokenBackend { return undefined; } - const queries = this._treeSitterLibraryService.getHighlightingQueriesSync(treeModel.languageId, reader); + const queries = this._treeSitterLibraryService.getHighlightingQueries(treeModel.languageId, reader); if (!queries) { return undefined; } diff --git a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts index 096cb8ded88..349045958c6 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts @@ -25,48 +25,32 @@ export interface ITreeSitterLibraryService { supportsLanguage(languageId: string, reader: IReader | undefined): boolean; /** - * Gets the Tree-sitter Language object. + * 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): Promise; - - /** - * Gets the Tree-sitter Language object synchronously. - * - * Note that This method runs synchronously and may fail if the language is - * not yet cached, as synchronous methods are required by editor APIs. - * @param languageId The language identifier to retrieve. - * @param reader Optional observable reader. - */ - getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined; + 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. - * - * Note that This method runs synchronously and may fail if the language is - * not yet cached, as synchronous methods are required by editor APIs. * @param languageId The language identifier to retrieve queries for. * @param reader Optional observable reader. */ - getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined; + getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined; /** * Gets the highlighting queries for a language. A return value of `null` * indicates that there are no highlights queries for this language. - * - * Note that This method runs synchronously and may fail if the language is - * not yet cached, as synchronous methods are required by editor APIs. * @param languageId The language identifier to retrieve queries for. * @param reader Optional observable reader. */ - getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined; + getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined; /** - * Creates a custom query for a language. Returns undefiend if + * Creates a one-off custom query for a language. * @param languageId The language identifier to create the query for. - * @param reader Optional observable reader. * @param querySource The query source string to compile. */ createQuery(languageId: string, querySource: string): Promise; diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts index 123cb671ee4..913a0b0fab1 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts @@ -18,19 +18,15 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer return false; } - async getLanguage(languageId: string): Promise { - throw new Error('not implemented in StandaloneTreeSitterLibraryService'); - } - - getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { + getLanguage(languageId: string, ignoreSupportsCheck: boolean, reader: IReader | undefined): Language | undefined { return undefined; } - getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { + getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } - getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { + getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } diff --git a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts index d90cfd0b67b..a28e7f3dd93 100644 --- a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts @@ -18,19 +18,15 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService { return false; } - async getLanguage(languageId: string): Promise { - throw new Error('not implemented in TestTreeSitterLibraryService'); - } - - getLanguageSync(languageId: string, reader: IReader | undefined): Language | undefined { + getLanguage(languageId: string, ignoreSupportsCheck: boolean, reader: IReader | undefined): Language | undefined { return undefined; } - getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { + getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } - getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { + getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { return null; } 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 ce6765a60da..3626204020e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -352,8 +352,6 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { // commands that would be auto approved if it were enabled. const actualCommand = toolEditedCommand ?? args.command; - const treeSitterLanguage = isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash; - let disclaimer: IMarkdownString | undefined; let customActions: ToolConfirmationAction[] | undefined; @@ -363,6 +361,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { 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); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts index b495aaa51e6..7a3f3dee46a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -5,6 +5,7 @@ import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { Lazy } from '../../../../../base/common/lazy.js'; +import { derived, waitForState } from '../../../../../base/common/observable.js'; import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; import type { Parser, Query } from '@vscode/tree-sitter-wasm'; @@ -33,7 +34,10 @@ export class TreeSitterCommandParser { async extractSubCommands(languageId: TreeSitterCommandParserLanguage, commandLine: string): Promise { const parser = await this._parser; - parser.setLanguage(await this._treeSitterLibraryService.getLanguage(languageId)); + const language = await waitForState(derived(reader => { + return this._treeSitterLibraryService.getLanguage(languageId, true, reader); + })); + parser.setLanguage(language); const tree = parser.parse(commandLine); if (!tree) { diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts index 82ef2193d31..0fb3a46221f 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts @@ -121,19 +121,15 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return treeSitter.Parser; } - async getLanguage(languageId: string): Promise { - return this._languagesCache.get(languageId).promise; - } - - getLanguageSync(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); return lang; } - getInjectionQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { + getInjectionQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { if (!this.supportsLanguage(languageId, reader)) { return undefined; } @@ -141,7 +137,7 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return query; } - getHighlightingQueriesSync(languageId: string, reader: IReader | undefined): Query | null | undefined { + getHighlightingQueries(languageId: string, reader: IReader | undefined): Query | null | undefined { if (!this.supportsLanguage(languageId, reader)) { return undefined; } From 258d0621411aa03e74feaa9f9763633e675f23f5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:08:25 -0700 Subject: [PATCH 12/20] Use Language in createQuery --- .../treeSitter/treeSitterLibraryService.ts | 4 ++-- .../standaloneTreeSitterLibraryService.ts | 2 +- .../services/testTreeSitterLibraryService.ts | 2 +- .../browser/treeSitterCommandParser.ts | 24 +++++++++---------- .../browser/treeSitterLibraryService.ts | 11 ++------- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts index 349045958c6..3c907113854 100644 --- a/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts +++ b/src/vs/editor/common/services/treeSitter/treeSitterLibraryService.ts @@ -50,8 +50,8 @@ export interface ITreeSitterLibraryService { /** * Creates a one-off custom query for a language. - * @param languageId The language identifier to create the query for. + * @param language The Language to create the query for. * @param querySource The query source string to compile. */ - createQuery(languageId: string, querySource: string): Promise; + createQuery(language: Language, querySource: string): Promise; } diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts index 913a0b0fab1..f724e9019ee 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterLibraryService.ts @@ -30,7 +30,7 @@ export class StandaloneTreeSitterLibraryService implements ITreeSitterLibrarySer return null; } - async createQuery(languageId: string, querySource: string): Promise { + async createQuery(language: Language, querySource: string): Promise { throw new Error('not implemented in StandaloneTreeSitterLibraryService'); } } diff --git a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts index a28e7f3dd93..b724892f883 100644 --- a/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts +++ b/src/vs/editor/test/common/services/testTreeSitterLibraryService.ts @@ -30,7 +30,7 @@ export class TestTreeSitterLibraryService implements ITreeSitterLibraryService { return null; } - async createQuery(languageId: string, querySource: string): Promise { + async createQuery(language: Language, querySource: string): Promise { throw new Error('not implemented in TestTreeSitterLibraryService'); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts index 7a3f3dee46a..44d4a4446a0 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/treeSitterCommandParser.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../../../base/common/errors.js'; -import { Lazy } from '../../../../../base/common/lazy.js'; import { derived, waitForState } from '../../../../../base/common/observable.js'; import { ITreeSitterLibraryService } from '../../../../../editor/common/services/treeSitter/treeSitterLibraryService.js'; -import type { Parser, Query } from '@vscode/tree-sitter-wasm'; +import type { Language, Parser, Query } from '@vscode/tree-sitter-wasm'; export const enum TreeSitterCommandParserLanguage { Bash = 'bash', @@ -16,15 +15,7 @@ export const enum TreeSitterCommandParserLanguage { export class TreeSitterCommandParser { private readonly _parser: Promise; - - private readonly _languageToQueryMap = { - [TreeSitterCommandParserLanguage.Bash]: new Lazy(() => { - return this._treeSitterLibraryService.createQuery(TreeSitterCommandParserLanguage.Bash, '(command) @command'); - }), - [TreeSitterCommandParserLanguage.PowerShell]: new Lazy(() => { - return this._treeSitterLibraryService.createQuery(TreeSitterCommandParserLanguage.PowerShell, '(command\ncommand_name: (command_name) @function)'); - }), - } satisfies { [K in TreeSitterCommandParserLanguage]: Lazy> }; + private readonly _queries: Map = new Map(); constructor( @ITreeSitterLibraryService private readonly _treeSitterLibraryService: ITreeSitterLibraryService @@ -44,7 +35,7 @@ export class TreeSitterCommandParser { throw new BugIndicatingError('Failed to parse tree'); } - const query = await this._languageToQueryMap[languageId].value; + const query = await this._getQuery(language); if (!query) { throw new BugIndicatingError('Failed to create tree sitter query'); } @@ -53,4 +44,13 @@ export class TreeSitterCommandParser { const subCommands = captures.map(e => e.node.text); return subCommands; } + + private async _getQuery(language: Language): Promise { + let query = this._queries.get(language); + if (!query) { + query = await this._treeSitterLibraryService.createQuery(language, '(command) @command'); + this._queries.set(language, query); + } + return query; + } } diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts index 0fb3a46221f..b6e82609b2d 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterLibraryService.ts @@ -145,15 +145,8 @@ export class TreeSitterLibraryService extends Disposable implements ITreeSitterL return query; } - async createQuery(languageId: string, querySource: string): Promise { - const [ - language, - treeSitter - ] = await Promise.all([ - this._languagesCache.get(languageId).promise, - this._treeSitterImport.value, - ]); - + async createQuery(language: Language, querySource: string): Promise { + const treeSitter = await this._treeSitterImport.value; return new treeSitter.Query(language, querySource); } } From 6ae9c550e306668c0a2cf1545eb827c6080d235d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:32:57 -0700 Subject: [PATCH 13/20] Get tree sitter import working in tests --- .../test/browser/runInTerminalTool.test.ts | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts index be45523bbe1..dd3bce6f4d1 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts @@ -24,6 +24,15 @@ 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'; + +// 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 = Promise.resolve(OperatingSystem.Windows); @@ -42,6 +51,7 @@ suite('RunInTerminalTool', () => { let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; + let fileService: IFileService; let storageService: IStorageService; let terminalServiceDisposeEmitter: Emitter; let chatServiceDisposeEmitter: Emitter<{ sessionId: string; reason: 'cleared' }>; @@ -50,13 +60,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(); 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 []; @@ -286,14 +308,14 @@ suite('RunInTerminalTool', () => { 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 })); }); } }); @@ -422,18 +444,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 }, From e1f2a0414f4dfd1b4efd85a542c37216e881d646 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:34:52 -0700 Subject: [PATCH 14/20] Sync tree sitter in remote package.json --- remote/package-lock.json | 8 ++++---- remote/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/remote/package-lock.json b/remote/package-lock.json index 238a0284ce0..09d6c70275c 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -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": { diff --git a/remote/package.json b/remote/package.json index 268bdf55729..41979903e57 100644 --- a/remote/package.json +++ b/remote/package.json @@ -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", From 462356a14d686ecf253b254a385cc41d68f01d56 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 08:40:30 -0700 Subject: [PATCH 15/20] Fix denial auto approve check We shouldn't show any rule suggestions if anything was denied --- .../browser/runInTerminalHelpers.ts | 9 ++++---- .../browser/tools/runInTerminalTool.ts | 6 ++--- .../test/browser/runInTerminalTool.test.ts | 22 +++++++++---------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 36cd3b1ab10..19330fc97ca 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -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'), 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 3626204020e..5e6ec0a8d6b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -59,7 +59,7 @@ function createPowerShellModelDescription(shell: string): string { '- Does NOT support multi-line commands', // Even for pwsh 7+ we want to use `;` to chain commands since the tree sitter grammar // doesn't parse `&&` - '- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly', + // '- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly', '- Prefer pipelines | for object-based data flow', '', 'Directory Management:', @@ -414,7 +414,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { // Log detailed auto approval reasoning for (const reason of autoApproveReasons) { - this._logService.info(`- ${reason}`); + this._logService.info(`RunInTerminalTool: autoApprove: - ${reason}`); } // Apply auto approval or force it off depending on enablement/opt-in state @@ -1001,7 +1001,7 @@ class BackgroundTerminalExecution extends Disposable { } } -class TerminalProfileFetcher { +export class TerminalProfileFetcher { readonly osBackend: Promise; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts index dd3bce6f4d1..1e03ba416ac 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts @@ -91,7 +91,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); @@ -341,7 +341,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 () => { @@ -354,7 +354,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 () => { @@ -367,7 +367,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 () => { @@ -514,7 +514,7 @@ suite('RunInTerminalTool', () => { explanation: 'Build the project' }); - assertConfirmationRequired(result, 'Run `pwsh` command?'); + assertConfirmationRequired(result, 'Run `bash` command?'); assertDropdownActions(result, [ { subCommand: 'npm run build' }, 'commandLine', @@ -558,7 +558,7 @@ suite('RunInTerminalTool', () => { explanation: 'Build the project' }); - assertConfirmationRequired(result, 'Run `pwsh` command?'); + assertConfirmationRequired(result, 'Run `bash` command?'); assertDropdownActions(result, [ 'configure', ]); @@ -570,7 +570,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', @@ -588,7 +588,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', @@ -620,7 +620,7 @@ suite('RunInTerminalTool', () => { explanation: 'Run multiple piped commands' }); - assertConfirmationRequired(result, 'Run `pwsh` command?'); + assertConfirmationRequired(result, 'Run `bash` command?'); assertDropdownActions(result, [ { subCommand: ['foo', 'bar'] }, 'commandLine', @@ -931,7 +931,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 () => { @@ -950,7 +950,7 @@ suite('RunInTerminalTool', () => { }); const result = await executeToolTest({ command: 'echo hello world' }); - assertConfirmationRequired(result, 'Run `pwsh` command?'); + assertConfirmationRequired(result, 'Run `bash` command?'); }); }); From 6f54e47dc86827f6228c1c53eec62abf3e4a8039 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:32:20 -0700 Subject: [PATCH 16/20] Reduce duplication in test --- .../test/browser/runInTerminalTool.test.ts | 86 ++++--------------- 1 file changed, 16 insertions(+), 70 deletions(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts index 1e03ba416ac..0102e1b7ca4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts @@ -290,19 +290,6 @@ 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', () => { @@ -970,66 +957,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().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, 'pwsh'); + }); }); }); }); From dafe2d8d38fd55c0fdd947d3a59cb39a8505d790 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:33:15 -0700 Subject: [PATCH 17/20] Sync tree sitter wasm in web --- remote/web/package-lock.json | 8 ++++---- remote/web/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index fab2167c05e..b3a80a4e8db 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -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": { diff --git a/remote/web/package.json b/remote/web/package.json index 8494a8b5090..46a1fefe151 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -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", From 30f6d3fd160d504416f35641e385723b7e7fc3a8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:55:14 -0700 Subject: [PATCH 18/20] Fix Linux getCopilotProfile test --- .../chatAgentTools/test/browser/runInTerminalTool.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts index 0102e1b7ca4..909f65a94df 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts @@ -974,7 +974,7 @@ suite('RunInTerminalTool', () => { const result = await runInTerminalTool.profileFetcher.getCopilotProfile(); strictEqual(typeof result, 'object'); - strictEqual((result as ITerminalProfile).path, 'pwsh'); + strictEqual((result as ITerminalProfile).path, 'bash'); }); }); }); From cbd176096363946cd82cbaaebc74a8c94ffe9b90 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:10:59 -0700 Subject: [PATCH 19/20] Remove line about multi-line commands These work now with tree sitter --- .../chatAgentTools/browser/tools/runInTerminalTool.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 5e6ec0a8d6b..0817703f554 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -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', // Even for pwsh 7+ we want to use `;` to chain commands since the tree sitter grammar - // doesn't parse `&&` - // '- Use semicolons ; to chain commands on one line, NEVER use && even when asked explicitly', + // 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:', From 9cdad99de22d170e725d09a543840b380ad814aa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:11:19 -0700 Subject: [PATCH 20/20] Move test to electron-browser to avoid web test problems --- .../{browser => electron-browser}/runInTerminalTool.test.ts | 2 ++ 1 file changed, 2 insertions(+) rename src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/{browser => electron-browser}/runInTerminalTool.test.ts (99%) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts similarity index 99% rename from src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts rename to src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 909f65a94df..62723155742 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -31,6 +31,8 @@ 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';