Skip to content

Commit 215b4c8

Browse files
committed
Merge branch 'main' into quiet-leopon
2 parents cd476de + cbeae1b commit 215b4c8

File tree

17 files changed

+403
-66
lines changed

17 files changed

+403
-66
lines changed

src/vs/base/common/async.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2378,7 +2378,7 @@ class ProducerConsumer<T> {
23782378
export class AsyncIterableProducer<T> implements AsyncIterable<T> {
23792379
private readonly _producerConsumer = new ProducerConsumer<IteratorResult<T>>();
23802380

2381-
constructor(executor: AsyncIterableExecutor<T>) {
2381+
constructor(executor: AsyncIterableExecutor<T>, private readonly _onReturn?: () => void) {
23822382
queueMicrotask(async () => {
23832383
const p = executor({
23842384
emitOne: value => this._producerConsumer.produce({ ok: true, value: { done: false, value: value } }),
@@ -2411,6 +2411,10 @@ export class AsyncIterableProducer<T> implements AsyncIterable<T> {
24112411

24122412
private readonly _iterator: AsyncIterator<T, void, void> = {
24132413
next: () => this._producerConsumer.consume(),
2414+
return: () => {
2415+
this._onReturn?.();
2416+
return Promise.resolve({ done: true, value: undefined });
2417+
},
24142418
throw: async (e) => {
24152419
this._finishError(e);
24162420
return { done: true, value: undefined };

src/vs/workbench/contrib/chat/browser/actions/chatActions.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,6 @@ export function registerChatActions() {
788788
true
789789
);
790790
return;
791-
} else if ((item as ICodingAgentPickerItem).uri !== undefined) {
792791
} else if ((item as ICodingAgentPickerItem).uri !== undefined) {
793792
// TODO: handle click
794793
return;

src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,6 @@ class BuiltinDynamicCompletions extends Disposable {
869869
private async addFileAndFolderEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) {
870870

871871
const makeCompletionItem = (resource: URI, kind: FileKind, description?: string, boostPriority?: boolean): CompletionItem => {
872-
console.log('makeCompletionItem', resource);
873872
const basename = this.labelService.getUriBasenameLabel(resource);
874873
const text = `${chatVariableLeader}file:${basename}`;
875874
const uriLabel = this.labelService.getUriLabel(resource, { relative: true });

src/vs/workbench/contrib/chat/browser/media/chat.css

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,9 +1877,6 @@ have to be updated for changes to the rules above, or to support more deeply nes
18771877
.interactive-item-container.interactive-request {
18781878
align-items: flex-end;
18791879

1880-
.native-edit-context {
1881-
outline: none;
1882-
}
18831880
}
18841881

18851882
.interactive-item-container.interactive-request:not(.editing):hover .request-hover {
@@ -2108,6 +2105,10 @@ have to be updated for changes to the rules above, or to support more deeply nes
21082105

21092106
.interactive-request.editing {
21102107
padding: 0px;
2108+
2109+
.interactive-input-part .chat-input-container .interactive-input-editor .monaco-editor .native-edit-context {
2110+
height: 0px !important;
2111+
}
21112112
}
21122113
}
21132114

src/vs/workbench/contrib/chat/browser/media/chatViewWelcome.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
}
1414
}
1515

16+
.interactive-session.experimental-welcome-view .interactive-input-part .dropdown-action-container {
17+
display: none;
18+
}
19+
20+
.interactive-session.experimental-welcome-view .interactive-input-part .chat-attachments-container {
21+
display: none;
22+
}
23+
1624
/* Container for chat widget welcome message */
1725
.interactive-session .chat-welcome-view-container {
1826
display: flex;
@@ -30,6 +38,10 @@
3038
display: none;
3139
}
3240

41+
.interactive-session.experimental-welcome-view .chat-input-toolbars .action-item:not(:has(.monaco-dropdown-with-primary)) {
42+
display: none;
43+
}
44+
3345
/* Container for ChatViewPane welcome view */
3446
.pane-body > .chat-view-welcome {
3547
flex-direction: column;

src/vs/workbench/contrib/mcp/common/mcpServer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { AsyncIterableObject, raceCancellationError, Sequencer } from '../../../../base/common/async.js';
6+
import { AsyncIterableProducer, raceCancellationError, Sequencer } from '../../../../base/common/async.js';
77
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
88
import { Iterable } from '../../../../base/common/iterator.js';
99
import * as json from '../../../../base/common/json.js';
@@ -441,7 +441,7 @@ export class McpServer extends Disposable implements IMcpServer {
441441

442442
public resources(token?: CancellationToken): AsyncIterable<IMcpResource[]> {
443443
const cts = new CancellationTokenSource(token);
444-
return new AsyncIterableObject<IMcpResource[]>(async emitter => {
444+
return new AsyncIterableProducer<IMcpResource[]>(async emitter => {
445445
await McpServer.callOn(this, async (handler) => {
446446
for await (const resource of handler.listResourcesIterable({}, cts.token)) {
447447
emitter.emitOne(resource.map(r => new McpResource(this, r)));

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts

Lines changed: 82 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ import { IConfigurationService } from '../../../../../platform/configuration/com
99
import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js';
1010
import { isPowerShell } from './runInTerminalHelpers.js';
1111

12+
interface IAutoApproveRule {
13+
regex: RegExp;
14+
sourceText: string;
15+
}
16+
1217
export class CommandLineAutoApprover extends Disposable {
13-
private _denyListRegexes: RegExp[] = [];
14-
private _allowListRegexes: RegExp[] = [];
18+
private _denyListRules: IAutoApproveRule[] = [];
19+
private _allowListRules: IAutoApproveRule[] = [];
20+
private _allowListCommandLineRules: IAutoApproveRule[] = [];
21+
private _denyListCommandLineRules: IAutoApproveRule[] = [];
1522

1623
constructor(
1724
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -26,30 +33,49 @@ export class CommandLineAutoApprover extends Disposable {
2633
}
2734

2835
updateConfiguration() {
29-
const { denyList, allowList } = this._mapAutoApproveConfigToRegexList(this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApprove));
30-
this._allowListRegexes = allowList;
31-
this._denyListRegexes = denyList;
36+
const { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules } = this._mapAutoApproveConfigToRules(this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApprove));
37+
this._allowListRules = allowListRules;
38+
this._denyListRules = denyListRules;
39+
this._allowListCommandLineRules = allowListCommandLineRules;
40+
this._denyListCommandLineRules = denyListCommandLineRules;
3241
}
3342

34-
isAutoApproved(command: string, shell: string, os: OperatingSystem): boolean {
43+
isCommandAutoApproved(command: string, shell: string, os: OperatingSystem): { isAutoApproved: boolean; reason: string } {
3544
// Check the deny list to see if this command requires explicit approval
36-
for (const regex of this._denyListRegexes) {
37-
if (this._commandMatchesRegex(regex, command, shell, os)) {
38-
return false;
45+
for (const rule of this._denyListRules) {
46+
if (this._commandMatchesRegex(rule.regex, command, shell, os)) {
47+
return { isAutoApproved: false, reason: `Command '${command}' is denied by deny list rule: ${rule.sourceText}` };
3948
}
4049
}
4150

4251
// Check the allow list to see if the command is allowed to run without explicit approval
43-
for (const regex of this._allowListRegexes) {
44-
if (this._commandMatchesRegex(regex, command, shell, os)) {
45-
return true;
52+
for (const rule of this._allowListRules) {
53+
if (this._commandMatchesRegex(rule.regex, command, shell, os)) {
54+
return { isAutoApproved: true, reason: `Command '${command}' is approved by allow list rule: ${rule.sourceText}` };
4655
}
4756
}
4857

4958
// TODO: LLM-based auto-approval https://github.com/microsoft/vscode/issues/253267
5059

5160
// Fallback is always to require approval
52-
return false;
61+
return { isAutoApproved: false, reason: `Command '${command}' has no matching auto approve entries` };
62+
}
63+
64+
isCommandLineAutoApproved(commandLine: string): { isAutoApproved: boolean; reason: string } {
65+
// Check the deny list first to see if this command line requires explicit approval
66+
for (const rule of this._denyListCommandLineRules) {
67+
if (rule.regex.test(commandLine)) {
68+
return { isAutoApproved: false, reason: `Command line '${commandLine}' is denied by deny list rule: ${rule.sourceText}` };
69+
}
70+
}
71+
72+
// Check if the full command line matches any of the allow list command line regexes
73+
for (const rule of this._allowListCommandLineRules) {
74+
if (rule.regex.test(commandLine)) {
75+
return { isAutoApproved: true, reason: `Command line '${commandLine}' is approved by allow list rule: ${rule.sourceText}` };
76+
}
77+
}
78+
return { isAutoApproved: false, reason: `Command line '${commandLine}' has no matching auto approve entries` };
5379
}
5480

5581
private _commandMatchesRegex(regex: RegExp, command: string, shell: string, os: OperatingSystem): boolean {
@@ -65,27 +91,63 @@ export class CommandLineAutoApprover extends Disposable {
6591
return false;
6692
}
6793

68-
private _mapAutoApproveConfigToRegexList(config: unknown): { denyList: RegExp[]; allowList: RegExp[] } {
94+
private _mapAutoApproveConfigToRules(config: unknown): {
95+
denyListRules: IAutoApproveRule[];
96+
allowListRules: IAutoApproveRule[];
97+
allowListCommandLineRules: IAutoApproveRule[];
98+
denyListCommandLineRules: IAutoApproveRule[];
99+
} {
69100
if (!config || typeof config !== 'object') {
70-
return { denyList: [], allowList: [] };
101+
return {
102+
denyListRules: [],
103+
allowListRules: [],
104+
allowListCommandLineRules: [],
105+
denyListCommandLineRules: []
106+
};
71107
}
72108

73-
const denyList: RegExp[] = [];
74-
const allowList: RegExp[] = [];
109+
const denyListRules: IAutoApproveRule[] = [];
110+
const allowListRules: IAutoApproveRule[] = [];
111+
const allowListCommandLineRules: IAutoApproveRule[] = [];
112+
const denyListCommandLineRules: IAutoApproveRule[] = [];
75113

76114
Object.entries(config).forEach(([key, value]) => {
77115
if (typeof value === 'boolean') {
78116
const regex = this._convertAutoApproveEntryToRegex(key);
79117
// IMPORTANT: Only true and false are used, null entries need to be ignored
80118
if (value === true) {
81-
allowList.push(regex);
119+
allowListRules.push({ regex, sourceText: key });
82120
} else if (value === false) {
83-
denyList.push(regex);
121+
denyListRules.push({ regex, sourceText: key });
122+
}
123+
} else if (typeof value === 'object' && value !== null) {
124+
// Handle object format like { approve: true/false, matchCommandLine: true/false }
125+
const objectValue = value as { approve?: boolean; matchCommandLine?: boolean };
126+
if (typeof objectValue.approve === 'boolean') {
127+
const regex = this._convertAutoApproveEntryToRegex(key);
128+
if (objectValue.approve === true) {
129+
if (objectValue.matchCommandLine === true) {
130+
allowListCommandLineRules.push({ regex, sourceText: key });
131+
} else {
132+
allowListRules.push({ regex, sourceText: key });
133+
}
134+
} else if (objectValue.approve === false) {
135+
if (objectValue.matchCommandLine === true) {
136+
denyListCommandLineRules.push({ regex, sourceText: key });
137+
} else {
138+
denyListRules.push({ regex, sourceText: key });
139+
}
140+
}
84141
}
85142
}
86143
});
87144

88-
return { denyList, allowList };
145+
return {
146+
denyListRules,
147+
allowListRules,
148+
allowListCommandLineRules,
149+
denyListCommandLineRules
150+
};
89151
}
90152

91153
private _convertAutoApproveEntryToRegex(value: string): RegExp {

src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalTool.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,33 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
171171
const subCommands = splitCommandLineIntoSubCommands(args.command, shell, os);
172172
const inlineSubCommands = subCommands.map(e => Array.from(extractInlineSubCommands(e, shell, os))).flat();
173173
const allSubCommands = [...subCommands, ...inlineSubCommands];
174-
if (allSubCommands.every(e => this._commandLineAutoApprover.isAutoApproved(e, shell, os))) {
174+
const subCommandResults = allSubCommands.map(e => this._commandLineAutoApprover.isCommandAutoApproved(e, shell, os));
175+
const autoApproveReasons: string[] = [...subCommandResults.map(e => e.reason)];
176+
177+
if (subCommandResults.every(e => e.isAutoApproved)) {
178+
this._logService.info('autoApprove: All sub-commands auto-approved');
175179
confirmationMessages = undefined;
176180
} else {
177-
confirmationMessages = {
178-
title: args.isBackground
179-
? localize('runInTerminal.background', "Run command in background terminal")
180-
: localize('runInTerminal.foreground', "Run command in terminal"),
181-
message: new MarkdownString(
182-
args.explanation
183-
),
184-
};
181+
this._logService.info('autoApprove: All sub-commands NOT auto-approved');
182+
const commandLineResults = this._commandLineAutoApprover.isCommandLineAutoApproved(args.command);
183+
autoApproveReasons.push(commandLineResults.reason);
184+
if (commandLineResults.isAutoApproved) {
185+
this._logService.info('autoApprove: Command line auto-approved');
186+
confirmationMessages = undefined;
187+
} else {
188+
this._logService.info('autoApprove: Command line NOT auto-approved');
189+
confirmationMessages = {
190+
title: args.isBackground
191+
? localize('runInTerminal.background', "Run command in background terminal")
192+
: localize('runInTerminal.foreground', "Run command in terminal"),
193+
message: new MarkdownString(args.explanation),
194+
};
195+
}
196+
}
197+
198+
// TODO: Surface reason on tool part https://github.com/microsoft/vscode/pull/256793
199+
for (const reason of autoApproveReasons) {
200+
this._logService.info(`- ${reason}`);
185201
}
186202
}
187203

src/vs/workbench/contrib/terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.ts

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import type { IStringDictionary } from '../../../../../base/common/collections.js';
7+
import type { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
78
import { localize } from '../../../../../nls.js';
89
import type { IConfigurationPropertySchema } from '../../../../../platform/configuration/common/configurationRegistry.js';
910

@@ -15,23 +16,61 @@ export interface ITerminalChatAgentToolsConfiguration {
1516
autoApprove: { [key: string]: boolean };
1617
}
1718

19+
const autoApproveBoolean: IJSONSchema = {
20+
type: 'boolean',
21+
enum: [
22+
true,
23+
false,
24+
],
25+
enumDescriptions: [
26+
localize('autoApprove.true', "Automatically approve the pattern."),
27+
localize('autoApprove.false', "Require explicit approval for the pattern."),
28+
],
29+
description: localize('autoApprove.key', "The start of a command to match against. A regular expression can be provided by wrapping the string in `/` characters."),
30+
};
31+
1832
export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurationPropertySchema> = {
1933
[TerminalChatAgentToolsSettingId.AutoApprove]: {
20-
markdownDescription: localize('autoApprove', "A list of commands or regular expressions that control whether the run in terminal tool commands require explicit approval. These will be matched against the start of a command. A regular expression can be provided by wrapping the string in `/` characters followed by optional flags such as `i`.\n\nSet to `true` to automatically approve commands, `false` to always require explicit approval or `null` to unset the value.\n\nExamples:\n- `\"mkdir\": true` Will allow all command lines starting with `mkdir`\n- `\"npm run build\": true` Will allow all command lines starting with `npm run build`\n- `\"rm\": false` Will require explicit approval for all command lines starting with `rm`\n- `\"/^git (status|show\\b.*)$/\": true` will allow `git status` and all command lines starting with `git show`\n- `\"/^Get-ChildItem\\b/i\": true` will allow Get-ChildItem command regardless of casing\n- `\"/.*/\": true` will allow all command lines\n- `\"rm\": null` will unset the default `false` value for `rm`\n\nNote that these commands and regular expressions are evaluated for every _sub-command_ within a single command line, so `foo && bar` for example will need both `foo` and `bar` to match a `true` entry and must not match a `false` entry in order to auto approve. Inline commands are also detected so `echo $(rm file)` will need both `echo $(rm file)` and `rm file` to pass."),
34+
markdownDescription: [
35+
localize('autoApprove.description.intro', "A list of commands or regular expressions that control whether the run in terminal tool commands require explicit approval. These will be matched against the start of a command. A regular expression can be provided by wrapping the string in {0} characters followed by optional flags such as {1} for case-insensitivity.", '`/`', '`i`'),
36+
localize('autoApprove.description.values', "Set to {0} to automatically approve commands, {1} to always require explicit approval or {2} to unset the value.", '`true`', '`false`', '`null`'),
37+
localize('autoApprove.description.subCommands', "Note that these commands and regular expressions are evaluated for every _sub-command_ within the full _command line_, so {0} for example will need both {1} and {2} to match a {3} entry and must not match a {4} entry in order to auto approve. Inline commands are also detected so {5} will need both {5} and {6} to pass.", '`foo && bar`', '`foo`', '`bar`', '`true`', '`false`', '`echo $(rm file)`', '`rm file`'),
38+
localize('autoApprove.description.commandLine', "An object can be used to match against the full command line instead of matching sub-commands and inline commands, for example {0}. This will be checked _after_ sub-commands are checked, taking precedence over even denied sub-commands.", '`{ approve: false, matchCommandLine: true }`'),
39+
[
40+
localize('autoApprove.description.examples.title', 'Examples:'),
41+
`|${localize('autoApprove.description.examples.value', "Value")}|${localize('autoApprove.description.examples.description', "Description")}|`,
42+
'|---|---|',
43+
'| `\"mkdir\": true` | ' + localize('autoApprove.description.examples.mkdir', "Allow all commands starting with {0}", '`mkdir`'),
44+
'| `\"npm run build\": true` | ' + localize('autoApprove.description.examples.npmRunBuild', "Allow all commands starting with {0}", '`npm run build`'),
45+
'| `\"/^git (status\\|show\\b.*)$/\": true` | ' + localize('autoApprove.description.examples.regexGit', "Allow {0} and all commands starting with {1}", '`git status`', '`git show`'),
46+
'| `\"/^Get-ChildItem\\b/i\": true` | ' + localize('autoApprove.description.examples.regexCase', "will allow {0} commands regardless of casing", '`Get-ChildItem`'),
47+
'| `\"/.*/\": true` | ' + localize('autoApprove.description.examples.regexAll', "Allow all commands (denied commands still require approval)"),
48+
'| `\"rm\": false` | ' + localize('autoApprove.description.examples.rm', "Require explicit approval for all commands starting with {0}", '`rm`'),
49+
'| `\"/\.ps1/i\": { approve: false, matchCommandLine: true }` | ' + localize('autoApprove.description.examples.ps1', "Require explicit approval for any _command line_ that contains {0} regardless of casing", '`".ps1"`'),
50+
'| `\"rm\": null` | ' + localize('autoApprove.description.examples.rmUnset', "Unset the default {0} value for {1}", '`false`', '`rm`'),
51+
].join('\n')
52+
].join('\n\n'),
2153
type: 'object',
2254
additionalProperties: {
2355
anyOf: [
56+
autoApproveBoolean,
2457
{
25-
type: 'boolean',
26-
enum: [
27-
true,
28-
false,
29-
],
30-
enumDescriptions: [
31-
localize('autoApprove.true', "Automatically approve the pattern."),
32-
localize('autoApprove.false', "Require explicit approval for the pattern."),
33-
],
34-
description: localize('autoApprove.key', "The start of a command to match against. A regular expression can be provided by wrapping the string in `/` characters."),
58+
type: 'object',
59+
properties: {
60+
approve: autoApproveBoolean,
61+
matchCommandLine: {
62+
type: 'boolean',
63+
enum: [
64+
true,
65+
false,
66+
],
67+
enumDescriptions: [
68+
localize('autoApprove.matchCommandLine.true', "Match against the full command line, eg. `foo && bar`."),
69+
localize('autoApprove.matchCommandLine.false', "Match against sub-commands and inline commands, eg. `foo && bar` will need both `foo` and `bar` to match."),
70+
],
71+
description: localize('autoApprove.matchCommandLine', "Whether to match against the full command line, as opposed to splitting by sub-commands and inline commands."),
72+
}
73+
}
3574
},
3675
{
3776
type: 'null',

0 commit comments

Comments
 (0)