From db5a12ed459ddd333f700104072474face3732ac Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:11:05 -0800 Subject: [PATCH 1/3] Allow approving terminal tool for session Fixes #260819 --- .../chatTerminalToolConfirmationSubPart.ts | 12 +++++++++- .../contrib/terminal/browser/terminal.ts | 14 +++++++++++ .../chat/browser/terminalChatService.ts | 23 +++++++++++++++++++ .../browser/runInTerminalHelpers.ts | 12 ++++++++++ .../commandLineAnalyzer.ts | 1 + .../commandLineAutoApproveAnalyzer.ts | 12 ++++++++++ .../browser/tools/runInTerminalTool.ts | 1 + .../commandLineFileWriteAnalyzer.test.ts | 18 ++++++++++----- 8 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 74cd04b8333..7606451936c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -30,6 +30,7 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common import { IMarkdownRenderer } from '../../../../../../platform/markdown/browser/markdownRenderer.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { IPreferencesService } from '../../../../../services/preferences/common/preferences.js'; +import { ITerminalChatService } from '../../../../terminal/browser/terminal.js'; import { TerminalContribSettingId } from '../../../../terminal/terminalContribExports.js'; import { migrateLegacyTerminalToolSpecificData } from '../../../common/chat.js'; import { ChatContextKeys } from '../../../common/chatContextKeys.js'; @@ -61,7 +62,8 @@ export type TerminalNewAutoApproveButtonData = ( { type: 'enable' } | { type: 'configure' } | { type: 'skip' } | - { type: 'newRule'; rule: ITerminalNewAutoApproveRule | ITerminalNewAutoApproveRule[] } + { type: 'newRule'; rule: ITerminalNewAutoApproveRule | ITerminalNewAutoApproveRule[] } | + { type: 'sessionApproval' } ); export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationSubPart { @@ -87,6 +89,7 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IStorageService private readonly storageService: IStorageService, + @ITerminalChatService private readonly terminalChatService: ITerminalChatService, @ITextModelService textModelService: ITextModelService, @IHoverService hoverService: IHoverService, ) { @@ -302,6 +305,13 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS doComplete = false; break; } + case 'sessionApproval': { + const sessionId = this.context.element.sessionId; + this.terminalChatService.setChatSessionAutoApproval(sessionId, true); + terminalData.autoApproveInfo = new MarkdownString(localize('sessionApproval', 'All commands will be auto approved for this session')); + toolConfirmKind = ToolConfirmKind.UserAction; + break; + } } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index a3a4c887fd8..e6794edc386 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -172,6 +172,20 @@ export interface ITerminalChatService { clearFocusedChatTerminalToolProgressPart(part: IChatTerminalToolProgressPart): void; getFocusedChatTerminalToolProgressPart(): IChatTerminalToolProgressPart | undefined; getMostRecentChatTerminalToolProgressPart(): IChatTerminalToolProgressPart | undefined; + + /** + * Enable or disable auto approval for all commands in a specific session. + * @param chatSessionId The chat session ID + * @param enabled Whether to enable or disable session auto approval + */ + setChatSessionAutoApproval(chatSessionId: string, enabled: boolean): void; + + /** + * Check if a session has auto approval enabled for all commands. + * @param chatSessionId The chat session ID + * @returns True if the session has auto approval enabled + */ + hasChatSessionAutoApproval(chatSessionId: string): boolean; } /** diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index 12cc5c56809..16e934cf304 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -47,6 +47,12 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ private readonly _hasToolTerminalContext: IContextKey; private readonly _hasHiddenToolTerminalContext: IContextKey; + /** + * Tracks chat session IDs that have auto approval enabled for all commands. This is a temporary + * approval that lasts only for the duration of the session. + */ + private readonly _sessionAutoApprovalEnabled = new Set(); + constructor( @ILogService private readonly _logService: ILogService, @ITerminalService private readonly _terminalService: ITerminalService, @@ -83,6 +89,11 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); this._toolSessionIdByTerminalInstance.delete(instance); this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); + // Clean up session auto approval state + const sessionId = LocalChatSessionUri.parseLocalSessionId(e.sessionResource); + if (sessionId) { + this._sessionAutoApprovalEnabled.delete(sessionId); + } this._persistToStorage(); this._updateHasToolTerminalContextKeys(); } @@ -279,4 +290,16 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ const hiddenTerminalCount = this.getToolSessionTerminalInstances(true).length; this._hasHiddenToolTerminalContext.set(hiddenTerminalCount > 0); } + + setChatSessionAutoApproval(chatSessionId: string, enabled: boolean): void { + if (enabled) { + this._sessionAutoApprovalEnabled.add(chatSessionId); + } else { + this._sessionAutoApprovalEnabled.delete(chatSessionId); + } + } + + hasChatSessionAutoApproval(chatSessionId: string): boolean { + return this._sessionAutoApprovalEnabled.has(chatSessionId); + } } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 19330fc97ca..1e9cb4911e9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -177,6 +177,18 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str actions.push(new Separator()); } + + // Allow all commands for this session + actions.push({ + label: localize('allowSession', 'Allow All Commands in this Session'), + tooltip: localize('allowSessionTooltip', 'Allow this tool to run in this session without confirmation.'), + data: { + type: 'sessionApproval' + } satisfies TerminalNewAutoApproveButtonData + }); + + actions.push(new Separator()); + // Always show configure option actions.push({ label: localize('autoApprove.configure', 'Configure Auto Approve...'), diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts index c062448457e..8a70c590465 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAnalyzer.ts @@ -21,6 +21,7 @@ export interface ICommandLineAnalyzerOptions { os: OperatingSystem; treeSitterLanguage: TreeSitterCommandParserLanguage; terminalToolSessionId: string; + chatSessionId: string | undefined; } export interface ICommandLineAnalyzerResult { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts index 36fbb02901e..5c99c634ef5 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts @@ -10,6 +10,7 @@ import type { SingleOrMany } from '../../../../../../../base/common/types.js'; import { localize } from '../../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { ITerminalChatService } from '../../../../../terminal/browser/terminal.js'; import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js'; import { TerminalToolConfirmationStorageKeys } from '../../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; import { openTerminalSettingsLinkCommandId } from '../../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.js'; @@ -43,12 +44,23 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @IStorageService private readonly _storageService: IStorageService, + @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, ) { super(); this._commandLineAutoApprover = this._register(instantiationService.createInstance(CommandLineAutoApprover)); } async analyze(options: ICommandLineAnalyzerOptions): Promise { + if (options.chatSessionId && this._terminalChatService.hasChatSessionAutoApproval(options.chatSessionId)) { + this._log('Session has auto approval enabled, auto approving command'); + return { + isAutoApproved: true, + isAutoApproveAllowed: true, + disclaimers: [], + autoApproveInfo: new MarkdownString(localize('autoApprove.session', 'Auto approved for this session')), + }; + } + let subCommands: string[] | undefined; try { subCommands = await this._treeSitterCommandParser.extractSubCommands(options.treeSitterLanguage, options.commandLine); 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 5f20d825407..8e0bb04becd 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/runInTerminalTool.ts @@ -412,6 +412,7 @@ export class RunInTerminalTool extends Disposable implements IToolImpl { shell, treeSitterLanguage: isPowerShell(shell, os) ? TreeSitterCommandParserLanguage.PowerShell : TreeSitterCommandParserLanguage.Bash, terminalToolSessionId, + chatSessionId: context.chatSessionId, }; const commandLineAnalyzerResults = await Promise.all(this._commandLineAnalyzers.map(e => e.analyze(commandLineAnalyzerOptions))); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts index 4d5fd7ea067..1cee4abbe5b 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/commandLineAnalyzer/commandLineFileWriteAnalyzer.test.ts @@ -79,7 +79,8 @@ suite('CommandLineFileWriteAnalyzer', () => { shell: 'bash', os: OperatingSystem.Linux, treeSitterLanguage: TreeSitterCommandParserLanguage.Bash, - terminalToolSessionId: 'test' + terminalToolSessionId: 'test', + chatSessionId: 'test', }; const result = await analyzer.analyze(options); @@ -144,7 +145,8 @@ suite('CommandLineFileWriteAnalyzer', () => { shell: 'bash', os: OperatingSystem.Linux, treeSitterLanguage: TreeSitterCommandParserLanguage.Bash, - terminalToolSessionId: 'test' + terminalToolSessionId: 'test', + chatSessionId: 'test', }; const result = await analyzer.analyze(options); @@ -180,7 +182,8 @@ suite('CommandLineFileWriteAnalyzer', () => { shell: 'pwsh', os: OperatingSystem.Windows, treeSitterLanguage: TreeSitterCommandParserLanguage.PowerShell, - terminalToolSessionId: 'test' + terminalToolSessionId: 'test', + chatSessionId: 'test', }; const result = await analyzer.analyze(options); @@ -255,7 +258,8 @@ suite('CommandLineFileWriteAnalyzer', () => { shell: 'bash', os: OperatingSystem.Linux, treeSitterLanguage: TreeSitterCommandParserLanguage.Bash, - terminalToolSessionId: 'test' + terminalToolSessionId: 'test', + chatSessionId: 'test', }; const result = await analyzer.analyze(options); @@ -286,7 +290,8 @@ suite('CommandLineFileWriteAnalyzer', () => { shell: 'bash', os: OperatingSystem.Linux, treeSitterLanguage: TreeSitterCommandParserLanguage.Bash, - terminalToolSessionId: 'test' + terminalToolSessionId: 'test', + chatSessionId: 'test', }; const result = await analyzer.analyze(options); @@ -314,7 +319,8 @@ suite('CommandLineFileWriteAnalyzer', () => { shell: 'bash', os: OperatingSystem.Linux, treeSitterLanguage: TreeSitterCommandParserLanguage.Bash, - terminalToolSessionId: 'test' + terminalToolSessionId: 'test', + chatSessionId: 'test', }; const result = await analyzer.analyze(options); From ed7e7f7ff62405fbb695690c52e3d2589b64b808 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 14 Nov 2025 12:21:04 -0800 Subject: [PATCH 2/3] Add unit tests for terminal session approval --- .../runInTerminalTool.test.ts | 100 +++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index 2f7bce6e548..d068e3d9327 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -33,7 +33,7 @@ import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/ch import { IChatService, type IChatTerminalToolInvocationData } from '../../../../chat/common/chatService.js'; import { LocalChatSessionUri } from '../../../../chat/common/chatUri.js'; import { ILanguageModelToolsService, IPreparedToolInvocation, IToolInvocationPreparationContext, type ToolConfirmationAction } from '../../../../chat/common/languageModelToolsService.js'; -import { ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; +import { ITerminalChatService, ITerminalService, type ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { ITerminalProfileResolverService } from '../../../../terminal/common/terminal.js'; import { RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser/tools/runInTerminalTool.js'; import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js'; @@ -106,6 +106,30 @@ suite('RunInTerminalTool', () => { getDefaultProfile: async () => ({ path: 'bash' } as ITerminalProfile) }); + // Stub ITerminalChatService with basic implementation + const sessionAutoApprovalMap = new Map(); + instantiationService.stub(ITerminalChatService, { + setChatSessionAutoApproval: (sessionId: string, enabled: boolean) => { + if (enabled) { + sessionAutoApprovalMap.set(sessionId, true); + } else { + sessionAutoApprovalMap.delete(sessionId); + } + }, + hasChatSessionAutoApproval: (sessionId: string) => { + return sessionAutoApprovalMap.has(sessionId); + }, + onDidRegisterTerminalInstanceWithToolSession: new Emitter().event + }); + + // Clean up session auto approval when session is disposed + chatServiceDisposeEmitter.event(e => { + const sessionId = LocalChatSessionUri.parseLocalSessionId(e.sessionResource); + if (sessionId) { + sessionAutoApprovalMap.delete(sessionId); + } + }); + storageService = instantiationService.get(IStorageService); storageService.store(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, true, StorageScope.APPLICATION, StorageTarget.USER); @@ -988,6 +1012,80 @@ suite('RunInTerminalTool', () => { }); }); + suite('session auto approval', () => { + test('should auto approve all commands when session has auto approval enabled', async () => { + const sessionId = 'test-session-123'; + const terminalChatService = instantiationService.get(ITerminalChatService); + terminalChatService.setChatSessionAutoApproval(sessionId, true); + + const context: IToolInvocationPreparationContext = { + parameters: { + command: 'rm dangerous-file.txt', + explanation: 'Remove a file', + isBackground: false + } as IRunInTerminalInputParams, + chatSessionId: sessionId + } as IToolInvocationPreparationContext; + + const result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); + assertAutoApproved(result); + + const terminalData = result!.toolSpecificData as IChatTerminalToolInvocationData; + ok(terminalData.autoApproveInfo, 'Expected autoApproveInfo to be defined'); + ok(terminalData.autoApproveInfo.value.includes('Auto approved for this session'), 'Expected session approval message'); + }); + + test('should require confirmation when session does not have auto approval', async () => { + const sessionId = 'test-session-456'; + + const context: IToolInvocationPreparationContext = { + parameters: { + command: 'rm file.txt', + explanation: 'Remove a file', + isBackground: false + } as IRunInTerminalInputParams, + chatSessionId: sessionId + } as IToolInvocationPreparationContext; + + const result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); + assertConfirmationRequired(result); + }); + + test('should clean up session auto approval when session is disposed', async () => { + const sessionId = 'test-session-789'; + const terminalChatService = instantiationService.get(ITerminalChatService); + + terminalChatService.setChatSessionAutoApproval(sessionId, true); + ok(terminalChatService.hasChatSessionAutoApproval(sessionId), 'Session should have auto approval enabled'); + + chatServiceDisposeEmitter.fire({ sessionResource: LocalChatSessionUri.forSession(sessionId), reason: 'cleared' }); + + ok(!terminalChatService.hasChatSessionAutoApproval(sessionId), 'Session auto approval should be cleaned up after disposal'); + }); + + test('should bypass rule checking when session has auto approval', async () => { + const sessionId = 'test-session-bypass'; + const terminalChatService = instantiationService.get(ITerminalChatService); + terminalChatService.setChatSessionAutoApproval(sessionId, true); + + setAutoApprove({ + rm: { approve: false } + }); + + const context: IToolInvocationPreparationContext = { + parameters: { + command: 'rm file.txt', + explanation: 'Remove a file', + isBackground: false + } as IRunInTerminalInputParams, + chatSessionId: sessionId + } as IToolInvocationPreparationContext; + + const result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); + assertAutoApproved(result); + }); + }); + suite('TerminalProfileFetcher', () => { suite('getCopilotProfile', () => { (isWindows ? test : test.skip)('should return custom profile when configured', async () => { From 3a194dbc0c470033cbb72a014b1d28d57cbe3c59 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:06:41 -0800 Subject: [PATCH 3/3] Remove bad tests, fix others --- .../chatTerminalToolConfirmationSubPart.ts | 10 +- .../chatTerminalToolProgressPart.ts | 6 + .../commandLineAutoApproveAnalyzer.ts | 8 +- .../runInTerminalTool.test.ts | 131 ++++++++---------- 4 files changed, 75 insertions(+), 80 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts index 7606451936c..e17e6487abe 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -43,7 +43,7 @@ import { ChatCustomConfirmationWidget, IChatConfirmationButton } from '../chatCo import { EditorPool } from '../chatContentCodePools.js'; import { IChatContentPartRenderContext } from '../chatContentParts.js'; import { ChatMarkdownContentPart } from '../chatMarkdownContentPart.js'; -import { openTerminalSettingsLinkCommandId } from './chatTerminalToolProgressPart.js'; +import { disableSessionAutoApprovalCommandId, openTerminalSettingsLinkCommandId } from './chatTerminalToolProgressPart.js'; import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; export const enum TerminalToolConfirmationStorageKeys { @@ -308,7 +308,13 @@ export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationS case 'sessionApproval': { const sessionId = this.context.element.sessionId; this.terminalChatService.setChatSessionAutoApproval(sessionId, true); - terminalData.autoApproveInfo = new MarkdownString(localize('sessionApproval', 'All commands will be auto approved for this session')); + const disableUri = createCommandUri(disableSessionAutoApprovalCommandId, sessionId); + const mdTrustSettings = { + isTrusted: { + enabledCommands: [disableSessionAutoApprovalCommandId] + } + }; + terminalData.autoApproveInfo = new MarkdownString(`${localize('sessionApproval', 'All commands will be auto approved for this session')} ([${localize('sessionApproval.disable', 'Disable')}](${disableUri.toString()}))`, mdTrustSettings); toolConfirmKind = ToolConfirmKind.UserAction; break; } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts index 05430826b56..a3cd5bb8c68 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -861,6 +861,7 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { }); export const openTerminalSettingsLinkCommandId = '_chat.openTerminalSettingsLink'; +export const disableSessionAutoApprovalCommandId = '_chat.disableSessionAutoApproval'; CommandsRegistry.registerCommand(openTerminalSettingsLinkCommandId, async (accessor, scopeRaw: string) => { const preferencesService = accessor.get(IPreferencesService); @@ -897,6 +898,11 @@ CommandsRegistry.registerCommand(openTerminalSettingsLinkCommandId, async (acces } }); +CommandsRegistry.registerCommand(disableSessionAutoApprovalCommandId, async (accessor, chatSessionId: string) => { + const terminalChatService = accessor.get(ITerminalChatService); + terminalChatService.setChatSessionAutoApproval(chatSessionId, false); +}); + class ToggleChatTerminalOutputAction extends Action implements IAction { private _expanded = false; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts index 5c99c634ef5..066391005f7 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/commandLineAutoApproveAnalyzer.ts @@ -53,11 +53,17 @@ export class CommandLineAutoApproveAnalyzer extends Disposable implements IComma async analyze(options: ICommandLineAnalyzerOptions): Promise { if (options.chatSessionId && this._terminalChatService.hasChatSessionAutoApproval(options.chatSessionId)) { this._log('Session has auto approval enabled, auto approving command'); + const disableUri = createCommandUri('_chat.disableSessionAutoApproval', options.chatSessionId); + const mdTrustSettings = { + isTrusted: { + enabledCommands: ['_chat.disableSessionAutoApproval'] + } + }; return { isAutoApproved: true, isAutoApproveAllowed: true, disclaimers: [], - autoApproveInfo: new MarkdownString(localize('autoApprove.session', 'Auto approved for this session')), + autoApproveInfo: new MarkdownString(`${localize('autoApprove.session', 'Auto approved for this session')} ([${localize('autoApprove.session.disable', 'Disable')}](${disableUri.toString()}))`, mdTrustSettings), }; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts index d068e3d9327..d4119d5fb3a 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts @@ -38,6 +38,7 @@ import { ITerminalProfileResolverService } from '../../../../terminal/common/ter import { RunInTerminalTool, type IRunInTerminalInputParams } from '../../browser/tools/runInTerminalTool.js'; import { ShellIntegrationQuality } from '../../browser/toolTerminalCreator.js'; import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../../common/terminalChatAgentToolsConfiguration.js'; +import { TerminalChatService } from '../../../chat/browser/terminalChatService.js'; class TestRunInTerminalTool extends RunInTerminalTool { protected override _osBackend: Promise = Promise.resolve(OperatingSystem.Windows); @@ -81,6 +82,7 @@ suite('RunInTerminalTool', () => { fileService: () => fileService, }, store); + instantiationService.stub(ITerminalChatService, store.add(instantiationService.createInstance(TerminalChatService))); instantiationService.stub(IWorkspaceContextService, workspaceContextService); instantiationService.stub(IHistoryService, { getLastActiveWorkspaceRoot: () => undefined @@ -106,30 +108,6 @@ suite('RunInTerminalTool', () => { getDefaultProfile: async () => ({ path: 'bash' } as ITerminalProfile) }); - // Stub ITerminalChatService with basic implementation - const sessionAutoApprovalMap = new Map(); - instantiationService.stub(ITerminalChatService, { - setChatSessionAutoApproval: (sessionId: string, enabled: boolean) => { - if (enabled) { - sessionAutoApprovalMap.set(sessionId, true); - } else { - sessionAutoApprovalMap.delete(sessionId); - } - }, - hasChatSessionAutoApproval: (sessionId: string) => { - return sessionAutoApprovalMap.has(sessionId); - }, - onDidRegisterTerminalInstanceWithToolSession: new Emitter().event - }); - - // Clean up session auto approval when session is disposed - chatServiceDisposeEmitter.event(e => { - const sessionId = LocalChatSessionUri.parseLocalSessionId(e.sessionResource); - if (sessionId) { - sessionAutoApprovalMap.delete(sessionId); - } - }); - storageService = instantiationService.get(IStorageService); storageService.store(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, true, StorageScope.APPLICATION, StorageTarget.USER); @@ -496,7 +474,7 @@ suite('RunInTerminalTool', () => { suite('prepareToolInvocation - custom actions for dropdown', () => { - function assertDropdownActions(result: IPreparedToolInvocation | undefined, items: ({ subCommand: SingleOrMany } | 'commandLine' | '---' | 'configure')[]) { + function assertDropdownActions(result: IPreparedToolInvocation | undefined, items: ({ subCommand: SingleOrMany } | 'commandLine' | '---' | 'configure' | 'sessionApproval')[]) { const actions = result?.confirmationMessages?.terminalCustomActions!; ok(actions, 'Expected custom actions to be defined'); @@ -511,6 +489,9 @@ suite('RunInTerminalTool', () => { if (item === 'configure') { strictEqual(action.label, 'Configure Auto Approve...'); strictEqual(action.data.type, 'configure'); + } else if (item === 'sessionApproval') { + strictEqual(action.label, 'Allow All Commands in this Session'); + strictEqual(action.data.type, 'sessionApproval'); } else if (item === 'commandLine') { strictEqual(action.label, 'Always Allow Exact Command Line'); strictEqual(action.data.type, 'newRule'); @@ -542,6 +523,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'npm run build' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -556,6 +539,8 @@ suite('RunInTerminalTool', () => { assertDropdownActions(result, [ { subCommand: 'foo' }, '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -583,6 +568,8 @@ suite('RunInTerminalTool', () => { assertConfirmationRequired(result, 'Run `bash` command?'); assertDropdownActions(result, [ + 'sessionApproval', + '---', 'configure', ]); }); @@ -598,6 +585,8 @@ suite('RunInTerminalTool', () => { { subCommand: ['npm install', 'npm run build'] }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -616,6 +605,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'foo' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -648,6 +639,8 @@ suite('RunInTerminalTool', () => { { subCommand: ['foo', 'bar'] }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -663,6 +656,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'git status' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -678,6 +673,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'npm test' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -693,6 +690,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'npm run build' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -708,6 +707,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'yarn run test' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -723,6 +724,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'foo' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -738,6 +741,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'npm run abc' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -753,6 +758,8 @@ suite('RunInTerminalTool', () => { { subCommand: ['npm run build', 'git status'] }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -768,6 +775,8 @@ suite('RunInTerminalTool', () => { { subCommand: ['git push', 'echo'] }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -783,6 +792,8 @@ suite('RunInTerminalTool', () => { { subCommand: ['git status', 'git log'] }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -798,6 +809,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'foo' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -810,6 +823,8 @@ suite('RunInTerminalTool', () => { assertConfirmationRequired(result); assertDropdownActions(result, [ + 'sessionApproval', + '---', 'configure', ]); }); @@ -825,6 +840,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'npm test' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -840,6 +857,8 @@ suite('RunInTerminalTool', () => { { subCommand: 'foo' }, 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -854,6 +873,8 @@ suite('RunInTerminalTool', () => { assertDropdownActions(result, [ 'commandLine', '---', + 'sessionApproval', + '---', 'configure', ]); }); @@ -870,6 +891,8 @@ suite('RunInTerminalTool', () => { assertConfirmationRequired(result); assertDropdownActions(result, [ + 'sessionApproval', + '---', 'configure', ]); }); @@ -1016,7 +1039,6 @@ suite('RunInTerminalTool', () => { test('should auto approve all commands when session has auto approval enabled', async () => { const sessionId = 'test-session-123'; const terminalChatService = instantiationService.get(ITerminalChatService); - terminalChatService.setChatSessionAutoApproval(sessionId, true); const context: IToolInvocationPreparationContext = { parameters: { @@ -1027,63 +1049,18 @@ suite('RunInTerminalTool', () => { chatSessionId: sessionId } as IToolInvocationPreparationContext; - const result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); + let result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); + assertConfirmationRequired(result); + + terminalChatService.setChatSessionAutoApproval(sessionId, true); + + result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); assertAutoApproved(result); const terminalData = result!.toolSpecificData as IChatTerminalToolInvocationData; ok(terminalData.autoApproveInfo, 'Expected autoApproveInfo to be defined'); ok(terminalData.autoApproveInfo.value.includes('Auto approved for this session'), 'Expected session approval message'); }); - - test('should require confirmation when session does not have auto approval', async () => { - const sessionId = 'test-session-456'; - - const context: IToolInvocationPreparationContext = { - parameters: { - command: 'rm file.txt', - explanation: 'Remove a file', - isBackground: false - } as IRunInTerminalInputParams, - chatSessionId: sessionId - } as IToolInvocationPreparationContext; - - const result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); - assertConfirmationRequired(result); - }); - - test('should clean up session auto approval when session is disposed', async () => { - const sessionId = 'test-session-789'; - const terminalChatService = instantiationService.get(ITerminalChatService); - - terminalChatService.setChatSessionAutoApproval(sessionId, true); - ok(terminalChatService.hasChatSessionAutoApproval(sessionId), 'Session should have auto approval enabled'); - - chatServiceDisposeEmitter.fire({ sessionResource: LocalChatSessionUri.forSession(sessionId), reason: 'cleared' }); - - ok(!terminalChatService.hasChatSessionAutoApproval(sessionId), 'Session auto approval should be cleaned up after disposal'); - }); - - test('should bypass rule checking when session has auto approval', async () => { - const sessionId = 'test-session-bypass'; - const terminalChatService = instantiationService.get(ITerminalChatService); - terminalChatService.setChatSessionAutoApproval(sessionId, true); - - setAutoApprove({ - rm: { approve: false } - }); - - const context: IToolInvocationPreparationContext = { - parameters: { - command: 'rm file.txt', - explanation: 'Remove a file', - isBackground: false - } as IRunInTerminalInputParams, - chatSessionId: sessionId - } as IToolInvocationPreparationContext; - - const result = await runInTerminalTool.prepareToolInvocation(context, CancellationToken.None); - assertAutoApproved(result); - }); }); suite('TerminalProfileFetcher', () => {