From b45d417e60ad24dda87df6f9de2b37d2d80b9361 Mon Sep 17 00:00:00 2001 From: justschen Date: Fri, 20 Feb 2026 21:23:20 -0800 Subject: [PATCH 01/25] autopilot mode + /yolo commands --- .../chat/browser/actions/chatActions.ts | 19 +++++ .../chatEditing/chatEditingSessionStorage.ts | 2 +- .../contrib/chat/browser/chatSlashCommands.ts | 31 +++++++ .../tools/languageModelToolsService.ts | 83 +++++++++++++++++++ .../browser/widget/input/chatInputPart.ts | 3 +- .../widget/input/modePickerActionItem.ts | 1 + .../chat/common/actions/chatContextKeys.ts | 1 + .../contrib/chat/common/chatModes.ts | 23 ++++- .../common/chatService/chatServiceImpl.ts | 1 + .../contrib/chat/common/constants.ts | 4 +- .../chat/common/editing/chatEditingService.ts | 2 +- .../contrib/chat/common/model/chatModel.ts | 3 +- .../chat/common/participants/chatAgents.ts | 4 +- .../languageProviders/promptValidator.ts | 2 +- .../common/promptSyntax/promptFileParser.ts | 5 ++ .../promptSyntax/service/promptsService.ts | 5 ++ .../service/promptsServiceImpl.ts | 3 +- .../common/tools/languageModelToolsService.ts | 21 +++++ .../tools/mockLanguageModelToolsService.ts | 10 +++ .../aiEditTelemetry/aiEditTelemetryService.ts | 2 + .../aiEditTelemetryServiceImpl.ts | 4 +- .../contrib/mcp/browser/mcpCommands.ts | 5 +- 22 files changed, 221 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index a5ed9ee7a9e58..ac427e39fbaf1 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1030,6 +1030,25 @@ export function registerChatActions() { } }); + registerAction2(class ToggleYoloModeAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.toggleYoloMode', + title: localize2('chat.toggleYoloMode', "Toggle YOLO Mode"), + category: CHAT_CATEGORY, + f1: true, + precondition: ChatContextKeys.enabled, + toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.GlobalAutoApprove}`, true), + }); + } + + async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const currentValue = configurationService.getValue(ChatConfiguration.GlobalAutoApprove); + await configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, !currentValue); + } + }); + const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id)); registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts index 0b8e1e0eff253..88816aa0df184 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -267,7 +267,7 @@ interface IModifiedEntryTelemetryInfoDTO { readonly command?: string; readonly modelId?: string; - readonly modeId?: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + readonly modeId?: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; readonly applyCodeBlockSuggestionId?: EditSuggestionId | undefined; readonly feature?: 'sideBarChat' | 'inlineChat' | undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index 297e6d823003b..4dd4aa28cd8ff 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -9,10 +9,12 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import * as nls from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IChatAgentService } from '../common/participants/chatAgents.js'; import { IChatSlashCommandService } from '../common/participants/chatSlashCommands.js'; import { IChatService } from '../common/chatService/chatService.js'; import { ChatAgentLocation, ChatModeKind } from '../common/constants.js'; +import { ILanguageModelToolsService } from '../common/tools/languageModelToolsService.js'; import { ACTION_ID_NEW_CHAT } from './actions/chatActions.js'; import { ChatSubmitAction, OpenModePickerAction, OpenModelPickerAction } from './actions/chatExecuteActions.js'; import { ConfigureToolsAction } from './actions/chatToolActions.js'; @@ -36,6 +38,8 @@ export class ChatSlashCommandsContribution extends Disposable { @IInstantiationService instantiationService: IInstantiationService, @IAgentSessionsService agentSessionsService: IAgentSessionsService, @IChatService chatService: IChatService, + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @INotificationService notificationService: INotificationService, ) { super(); this._store.add(slashCommandService.registerSlashCommand({ @@ -151,6 +155,33 @@ export class ChatSlashCommandsContribution extends Disposable { chatService.setChatSessionTitle(sessionResource, title); } })); + this._store.add(slashCommandService.registerSlashCommand({ + command: 'yolo', + detail: nls.localize('yolo', "Toggle auto-approval of all tool calls"), + sortText: 'z1_yolo', + executeImmediately: false, + silent: true, + locations: [ChatAgentLocation.Chat] + }, async (prompt, _progress, _history, _location, sessionResource) => { + const trimmed = prompt.trim(); + if (trimmed) { + // /yolo — enable for next request only, then submit + toolsService.enableNextRequestYolo(sessionResource); + const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); + if (widget) { + widget.acceptInput(trimmed); + } + } else { + // /yolo — toggle session YOLO mode + if (toolsService.isSessionYolo(sessionResource)) { + toolsService.disableSessionYolo(sessionResource); + notificationService.info(nls.localize('yolo.disabled', "YOLO mode disabled for this session")); + } else { + toolsService.enableSessionYolo(sessionResource); + notificationService.info(nls.localize('yolo.enabled', "YOLO mode enabled for this session — all tools will be auto-approved")); + } + } + })); this._store.add(slashCommandService.registerSlashCommand({ command: 'help', detail: '', diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 020f07ec8a215..f1ac47192fd90 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -110,6 +110,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo /** Pending tool calls in the streaming phase, keyed by toolCallId */ private readonly _pendingToolCalls = new Map(); + /** Sessions with YOLO mode enabled (keyed by session resource toString) */ + private readonly _yoloSessions = new Set(); + + /** Sessions with YOLO mode enabled for only the next request (keyed by session resource toString) */ + private readonly _yoloNextRequestSessions = new Set(); + private readonly _isAgentModeEnabled: IObservable; constructor( @@ -198,6 +204,26 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo description: localize('copilot.toolSet.agent.description', 'Delegate tasks to other agents'), } )); + + // Clean up YOLO state when sessions are disposed + this._register(this._chatService.onDidDisposeSession(e => { + for (const sessionResource of e.sessionResource) { + const key = sessionResource.toString(); + this._yoloSessions.delete(key); + this._yoloNextRequestSessions.delete(key); + } + })); + + // Clear next-request YOLO when a request completes + this._register(this._chatService.onDidCreateModel(model => { + const listener = model.onDidChange(e => { + if (e.kind === 'completedRequest') { + const key = model.sessionResource.toString(); + this._yoloNextRequestSessions.delete(key); + } + }); + model.onDidDispose(() => listener.dispose()); + })); } /** @@ -245,6 +271,22 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return false; } + enableSessionYolo(sessionResource: URI): void { + this._yoloSessions.add(sessionResource.toString()); + } + + disableSessionYolo(sessionResource: URI): void { + this._yoloSessions.delete(sessionResource.toString()); + } + + isSessionYolo(sessionResource: URI): boolean { + return this._yoloSessions.has(sessionResource.toString()); + } + + enableNextRequestYolo(sessionResource: URI): void { + this._yoloNextRequestSessions.add(sessionResource.toString()); + } + override dispose(): void { super.dispose(); @@ -1043,6 +1085,28 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } + // This tool always requires user confirmation (pre-agentic-loop options). + // Not even YOLO mode should bypass it. + if (toolId === toolIdThatCannotBeAutoApproved) { + return undefined; + } + + // Session-scoped YOLO mode (via /yolo slash command) bypasses per-tool + // eligibility settings — the user explicitly asked to auto-approve everything. + if (chatSessionResource) { + const key = chatSessionResource.toString(); + if (this._yoloSessions.has(key) || this._yoloNextRequestSessions.has(key)) { + return { type: ToolConfirmKind.Setting, id: 'chat.yolo.session' }; + } + + // Mode-level auto-approve (e.g. autopilot agent with auto-approve: true) + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (request?.modeInfo?.autoApprove) { + return { type: ToolConfirmKind.Setting, id: 'chat.mode.autoApprove' }; + } + } + if (!this.isToolEligibleForAutoApproval(tool.data)) { return undefined; } @@ -1075,6 +1139,25 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + if (toolId === toolIdThatCannotBeAutoApproved) { + return undefined; + } + + // Session-scoped YOLO mode bypasses all post-execution confirmations + if (chatSessionResource) { + const key = chatSessionResource.toString(); + if (this._yoloSessions.has(key) || this._yoloNextRequestSessions.has(key)) { + return { type: ToolConfirmKind.Setting, id: 'chat.yolo.session' }; + } + + // Mode-level auto-approve + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (request?.modeInfo?.autoApprove) { + return { type: ToolConfirmKind.Setting, id: 'chat.mode.autoApprove' }; + } + } + if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 07a2242918ee4..f545b0a880ecc 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -404,7 +404,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get currentModeInfo(): IChatRequestModeInfo { const mode = this._currentModeObservable.get(); - const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; + const modeId: 'ask' | 'agent' | 'edit' | 'autopilot' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; const modeInstructions = mode.modeInstructions?.get(); return { @@ -418,6 +418,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, + autoApprove: mode.autoApprove?.get(), }; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 8cc57f3238fd4..fbadbaee759df 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -51,6 +51,7 @@ const builtinDefaultIcon = (mode: IChatMode) => { case 'ask': return Codicon.ask; case 'edit': return Codicon.edit; case 'plan': return Codicon.tasklist; + case 'autopilot': return Codicon.rocket; default: return undefined; } }; diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index fd23452fa9bd3..de1c8e0fc8433 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -145,6 +145,7 @@ export namespace ChatContextKeyExprs { export const inEditingMode = ContextKeyExpr.or( ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), + ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Autopilot), ); /** diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 3885b17912750..d6cfff5a708e3 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -128,6 +128,7 @@ export class ChatModeService extends Disposable implements IChatModeService { target: cachedMode.target ?? Target.Undefined, visibility, agents: cachedMode.agents, + autoApprove: cachedMode.autoApprove, source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local } }; const instance = new CustomChatMode(customChatMode); @@ -220,6 +221,7 @@ export class ChatModeService extends Disposable implements IChatModeService { // But hide it if the user manually disabled it via settings if (this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy()) { builtinModes.unshift(ChatMode.Agent); + builtinModes.splice(1, 0, ChatMode.Autopilot); } builtinModes.push(ChatMode.Edit); return builtinModes; @@ -256,6 +258,7 @@ export interface IChatModeData { readonly visibility?: ICustomAgentVisibility; readonly agents?: readonly string[]; readonly infer?: boolean; // deprecated, only available in old cached data + readonly autoApprove?: boolean; } export interface IChatMode { @@ -276,6 +279,7 @@ export interface IChatMode { readonly target: IObservable; readonly visibility?: IObservable; readonly agents?: IObservable; + readonly autoApprove?: IObservable; } export interface IVariableReference { @@ -323,6 +327,7 @@ export class CustomChatMode implements IChatMode { private readonly _targetObservable: ISettableObservable; private readonly _visibilityObservable: ISettableObservable; private readonly _agentsObservable: ISettableObservable; + private readonly _autoApproveObservable: ISettableObservable; private _source: IAgentSource; public readonly id: string; @@ -387,6 +392,10 @@ export class CustomChatMode implements IChatMode { return this._agentsObservable; } + get autoApprove(): IObservable { + return this._autoApproveObservable; + } + public readonly kind = ChatModeKind.Agent; constructor( @@ -402,6 +411,7 @@ export class CustomChatMode implements IChatMode { this._targetObservable = observableValue('target', customChatMode.target); this._visibilityObservable = observableValue('visibility', customChatMode.visibility); this._agentsObservable = observableValue('agents', customChatMode.agents); + this._autoApproveObservable = observableValue('autoApprove', customChatMode.autoApprove); this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions); this._uriObservable = observableValue('uri', customChatMode.uri); this._source = customChatMode.source; @@ -421,6 +431,7 @@ export class CustomChatMode implements IChatMode { this._targetObservable.set(newData.target, tx); this._visibilityObservable.set(newData.visibility, tx); this._agentsObservable.set(newData.agents, tx); + this._autoApproveObservable.set(newData.autoApprove, tx); this._modeInstructions.set(newData.agentInstructions, tx); this._uriObservable.set(newData.uri, tx); this._source = newData.source; @@ -442,7 +453,8 @@ export class CustomChatMode implements IChatMode { source: serializeChatModeSource(this._source), target: this.target.get(), visibility: this.visibility.get(), - agents: this.agents.get() + agents: this.agents.get(), + autoApprove: this.autoApprove.get(), }; } } @@ -488,18 +500,23 @@ export class BuiltinChatMode implements IChatMode { public readonly description: IObservable; public readonly icon: IObservable; public readonly target: IObservable; + public readonly autoApprove?: IObservable; constructor( public readonly kind: ChatModeKind, label: string, description: string, icon: ThemeIcon, + options?: { autoApprove?: boolean }, ) { this.name = constObservable(kind); this.label = constObservable(label); this.description = observableValue('description', description); this.icon = constObservable(icon); this.target = constObservable(Target.Undefined); + if (options?.autoApprove) { + this.autoApprove = constObservable(true); + } } public get isBuiltin(): boolean { @@ -528,10 +545,12 @@ export namespace ChatMode { export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question); export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit); export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent); + export const Autopilot = new BuiltinChatMode(ChatModeKind.Autopilot, 'Autopilot', localize('autopilotDescription', "Auto-approve all tools and run autonomously"), Codicon.rocket, { autoApprove: true }); } export function isBuiltinChatMode(mode: IChatMode): boolean { return mode.id === ChatMode.Ask.id || mode.id === ChatMode.Edit.id || - mode.id === ChatMode.Agent.id; + mode.id === ChatMode.Agent.id || + mode.id === ChatMode.Autopilot.id; } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index e2b5b29c409fe..2612487545f03 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1038,6 +1038,7 @@ export class ChatService extends Disposable implements IChatService { enableCommandDetection && (location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) && options?.modeInfo?.kind !== ChatModeKind.Agent && + options?.modeInfo?.kind !== ChatModeKind.Autopilot && options?.modeInfo?.kind !== ChatModeKind.Edit && !options?.agentIdSilent ) { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index ed3f2318acc61..4fa82dddc723d 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -60,7 +60,8 @@ export enum ChatConfiguration { export enum ChatModeKind { Ask = 'ask', Edit = 'edit', - Agent = 'agent' + Agent = 'agent', + Autopilot = 'autopilot' } export function validateChatMode(mode: unknown): ChatModeKind | undefined { @@ -68,6 +69,7 @@ export function validateChatMode(mode: unknown): ChatModeKind | undefined { case ChatModeKind.Ask: case ChatModeKind.Edit: case ChatModeKind.Agent: + case ChatModeKind.Autopilot: return mode as ChatModeKind; default: return undefined; diff --git a/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts index 58b49b13fa570..2c255d8e70322 100644 --- a/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts @@ -70,7 +70,7 @@ export interface IModifiedEntryTelemetryInfo { readonly requestId: string; readonly result: IChatAgentResult | undefined; readonly modelId: string | undefined; - readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + readonly modeId: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined; readonly feature: 'sideBarChat' | 'inlineChat' | undefined; } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index c5d858de46c67..6f609ef8e527a 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -312,8 +312,9 @@ export interface IChatRequestModeInfo { kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' isBuiltin: boolean; modeInstructions: IChatRequestModeInstructions | undefined; - modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; + modeId: 'ask' | 'agent' | 'edit' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; + autoApprove?: boolean; } export interface IChatRequestModeInstructions { diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index c3c760b5d6a64..30734714d33c4 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -410,8 +410,10 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation, mode: ChatModeKind = ChatModeKind.Ask): IChatAgent | undefined { + // Autopilot uses the same agent as Agent mode + const effectiveMode = mode === ChatModeKind.Autopilot ? ChatModeKind.Agent : mode; return this._preferExtensionAgent(this.getActivatedAgents().filter(a => { - if (mode && !a.modes.includes(mode)) { + if (effectiveMode && !a.modes.includes(effectiveMode)) { return false; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 8b9a1b018cfe6..5493d570498db 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -707,7 +707,7 @@ function isTrueOrFalse(value: IValue): boolean { const allAttributeNames: Record = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], - [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation], + [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, PromptHeaderAttributes.autoApprove], [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation], [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index e11732310ec92..a1555805600e7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -85,6 +85,7 @@ export namespace PromptHeaderAttributes { export const userInvokable = 'user-invokable'; export const userInvocable = 'user-invocable'; export const disableModelInvocation = 'disable-model-invocation'; + export const autoApprove = 'auto-approve'; } export namespace GithubPromptHeaderAttributes { @@ -330,6 +331,10 @@ export class PromptHeader { return this.getBooleanAttribute(PromptHeaderAttributes.disableModelInvocation); } + public get autoApprove(): boolean | undefined { + return this.getBooleanAttribute(PromptHeaderAttributes.autoApprove); + } + private getBooleanAttribute(key: string): boolean | undefined { const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); if (attribute?.value.type === 'scalar') { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index 1ba51c3703aed..74aca35d67856 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -202,6 +202,11 @@ export interface ICustomAgent { */ readonly agents?: readonly string[]; + /** + * When true, all tool calls are auto-approved (YOLO mode) for this agent. + */ + readonly autoApprove?: boolean; + /** * Where the agent was loaded from. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index ded88d9804ab3..fb86b8650a848 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -624,7 +624,8 @@ export class PromptsService extends Disposable implements IPromptsService { if (target === Target.Claude && tools) { tools = mapClaudeTools(tools); } - return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source }; + const autoApprove = ast.header.autoApprove; + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, autoApprove, agentInstructions, source }; }) ); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index d14a93696e08e..a55f006114bb9 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -586,6 +586,27 @@ export interface ILanguageModelToolsService { toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[]; toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; + + /** + * Enable YOLO mode (auto-approve all tools) for the given chat session. + */ + enableSessionYolo(sessionResource: URI): void; + + /** + * Disable YOLO mode for the given chat session. + */ + disableSessionYolo(sessionResource: URI): void; + + /** + * Check whether YOLO mode is active for the given chat session. + */ + isSessionYolo(sessionResource: URI): boolean; + + /** + * Enable YOLO mode for just the next request in the given chat session. + * The flag is automatically cleared when the request completes. + */ + enableNextRequestYolo(sessionResource: URI): void; } diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index 7d1ca3200bca2..4182612968690 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -186,4 +186,14 @@ export class MockLanguageModelToolsService extends Disposable implements ILangua getDeprecatedFullReferenceNames(): Map> { throw new Error('Method not implemented.'); } + + enableSessionYolo(_sessionResource: URI): void { } + + disableSessionYolo(_sessionResource: URI): void { } + + isSessionYolo(_sessionResource: URI): boolean { + return false; + } + + enableNextRequestYolo(_sessionResource: URI): void { } } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts index ffef6e96b809c..20ce8b30580d0 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts @@ -52,6 +52,8 @@ export interface IEditTelemetryBaseData { | 'edit' /** AI agent mode for autonomous task completion and multi-file edits */ | 'agent' + /** Autonomous agent mode with all tools auto-approved */ + | 'autopilot' /** Custom mode defined by extensions or user settings */ | 'custom' /** Applying a previously suggested code block */ diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts index 9e753e0ed6fbb..3984825c19d26 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts @@ -42,7 +42,7 @@ export class AiEditTelemetryServiceImpl implements IAiEditTelemetryService { editLinesInserted: number | undefined; editLinesDeleted: number | undefined; - modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + modeId: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; modelId: TelemetryTrustedValue; applyCodeBlockSuggestionId: string | undefined; }, { @@ -113,7 +113,7 @@ export class AiEditTelemetryServiceImpl implements IAiEditTelemetryService { editLinesInserted: number | undefined; editLinesDeleted: number | undefined; - modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + modeId: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; modelId: TelemetryTrustedValue; applyCodeBlockSuggestionId: string | undefined; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 71fd9bcd6c820..f2aa5083b517b 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -89,7 +89,10 @@ export class ListMcpServerCommand extends Action2 { ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools), McpContextKeys.hasServersWithErrors, ), - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), + ContextKeyExpr.or( + ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), + ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Autopilot), + ), ChatContextKeys.lockedToCodingAgent.negate(), ChatContextKeys.Setup.hidden.negate(), ), From 7373f280553ff386db7bf01228634b35ef18c1cf Mon Sep 17 00:00:00 2001 From: justschen Date: Fri, 20 Feb 2026 21:46:33 -0800 Subject: [PATCH 02/25] fix tests --- .../contrib/chat/test/common/chatService/mockChatService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index 5d662e05199bb..58c2585e6a8a9 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -122,7 +122,7 @@ export class MockChatService implements IChatService { notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void { throw new Error('Method not implemented.'); } - readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!; + readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = Event.None; async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { throw new Error('Method not implemented.'); From 17a55b4ac70411a4b596f476bb4fe273db01ce93 Mon Sep 17 00:00:00 2001 From: justschen Date: Fri, 20 Feb 2026 21:49:45 -0800 Subject: [PATCH 03/25] revert fix --- .../contrib/chat/test/common/chatService/mockChatService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts index 58c2585e6a8a9..5d662e05199bb 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -122,7 +122,7 @@ export class MockChatService implements IChatService { notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void { throw new Error('Method not implemented.'); } - readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = Event.None; + readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!; async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { throw new Error('Method not implemented.'); From 1728c43919085373ac4ed69dee24179fcc8f70bc Mon Sep 17 00:00:00 2001 From: justschen Date: Fri, 20 Feb 2026 22:06:56 -0800 Subject: [PATCH 04/25] fix disposable leak --- .../tools/languageModelToolsService.ts | 54 ++++++++++++------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index f1ac47192fd90..ec2f7e2785c35 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -205,25 +205,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } )); - // Clean up YOLO state when sessions are disposed - this._register(this._chatService.onDidDisposeSession(e => { - for (const sessionResource of e.sessionResource) { - const key = sessionResource.toString(); - this._yoloSessions.delete(key); - this._yoloNextRequestSessions.delete(key); - } - })); - - // Clear next-request YOLO when a request completes - this._register(this._chatService.onDidCreateModel(model => { - const listener = model.onDidChange(e => { - if (e.kind === 'completedRequest') { - const key = model.sessionResource.toString(); - this._yoloNextRequestSessions.delete(key); - } - }); - model.onDidDispose(() => listener.dispose()); - })); } /** @@ -271,7 +252,40 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return false; } + private _yoloCleanupRegistered = false; + + private _ensureYoloCleanup(): void { + if (this._yoloCleanupRegistered) { + return; + } + this._yoloCleanupRegistered = true; + + // Clean up YOLO state when sessions are disposed + this._register(this._chatService.onDidDisposeSession(e => { + for (const sessionResource of e.sessionResource) { + const key = sessionResource.toString(); + this._yoloSessions.delete(key); + this._yoloNextRequestSessions.delete(key); + } + })); + } + + private _watchModelForRequestCompletion(sessionResource: URI): void { + const model = this._chatService.getSession(sessionResource); + if (!model) { + return; + } + const listener = model.onDidChange(e => { + if (e.kind === 'completedRequest') { + this._yoloNextRequestSessions.delete(sessionResource.toString()); + listener.dispose(); + } + }); + Event.once(model.onDidDispose)(() => listener.dispose()); + } + enableSessionYolo(sessionResource: URI): void { + this._ensureYoloCleanup(); this._yoloSessions.add(sessionResource.toString()); } @@ -284,7 +298,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } enableNextRequestYolo(sessionResource: URI): void { + this._ensureYoloCleanup(); this._yoloNextRequestSessions.add(sessionResource.toString()); + this._watchModelForRequestCompletion(sessionResource); } override dispose(): void { From 1d5276ceed2cfa6bfdcc7986101fab8999658ae8 Mon Sep 17 00:00:00 2001 From: justschen Date: Fri, 20 Feb 2026 22:23:28 -0800 Subject: [PATCH 05/25] address a few comments, make sure it works when switching sessions --- .../tools/languageModelToolsService.ts | 56 +++++++++++-------- .../languageProviders/promptValidator.ts | 12 ++++ 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index ec2f7e2785c35..bd49b6bcaddf0 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -114,7 +114,8 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo private readonly _yoloSessions = new Set(); /** Sessions with YOLO mode enabled for only the next request (keyed by session resource toString) */ - private readonly _yoloNextRequestSessions = new Set(); + /** Maps session key → request ID that consumed the flag (null = pending, not yet consumed) */ + private readonly _yoloNextRequestSessions = new Map(); private readonly _isAgentModeEnabled: IObservable; @@ -270,20 +271,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo })); } - private _watchModelForRequestCompletion(sessionResource: URI): void { - const model = this._chatService.getSession(sessionResource); - if (!model) { - return; - } - const listener = model.onDidChange(e => { - if (e.kind === 'completedRequest') { - this._yoloNextRequestSessions.delete(sessionResource.toString()); - listener.dispose(); - } - }); - Event.once(model.onDidDispose)(() => listener.dispose()); - } - enableSessionYolo(sessionResource: URI): void { this._ensureYoloCleanup(); this._yoloSessions.add(sessionResource.toString()); @@ -299,8 +286,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo enableNextRequestYolo(sessionResource: URI): void { this._ensureYoloCleanup(); - this._yoloNextRequestSessions.add(sessionResource.toString()); - this._watchModelForRequestCompletion(sessionResource); + this._yoloNextRequestSessions.set(sessionResource.toString(), null); } override dispose(): void { @@ -1111,15 +1097,29 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // eligibility settings — the user explicitly asked to auto-approve everything. if (chatSessionResource) { const key = chatSessionResource.toString(); - if (this._yoloSessions.has(key) || this._yoloNextRequestSessions.has(key)) { - return { type: ToolConfirmKind.Setting, id: 'chat.yolo.session' }; + if (this._yoloSessions.has(key)) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; + } + + if (this._yoloNextRequestSessions.has(key)) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + const consumedBy = this._yoloNextRequestSessions.get(key); + if (consumedBy === null && request) { + this._yoloNextRequestSessions.set(key, request.id); + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; + } else if (consumedBy !== null && consumedBy === request?.id) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; + } else { + this._yoloNextRequestSessions.delete(key); + } } // Mode-level auto-approve (e.g. autopilot agent with auto-approve: true) const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); if (request?.modeInfo?.autoApprove) { - return { type: ToolConfirmKind.Setting, id: 'chat.mode.autoApprove' }; + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'mode-auto-approve' }; } } @@ -1162,15 +1162,23 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Session-scoped YOLO mode bypasses all post-execution confirmations if (chatSessionResource) { const key = chatSessionResource.toString(); - if (this._yoloSessions.has(key) || this._yoloNextRequestSessions.has(key)) { - return { type: ToolConfirmKind.Setting, id: 'chat.yolo.session' }; + if (this._yoloSessions.has(key)) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; } - // Mode-level auto-approve const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); + + if (this._yoloNextRequestSessions.has(key)) { + const consumedBy = this._yoloNextRequestSessions.get(key); + if (consumedBy !== null && consumedBy === request?.id) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; + } + } + + // Mode-level auto-approve if (request?.modeInfo?.autoApprove) { - return { type: ToolConfirmKind.Setting, id: 'chat.mode.autoApprove' }; + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'mode-auto-approve' }; } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 5493d570498db..4ba8ca7170964 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -189,6 +189,7 @@ export class PromptValidator { this.validateUserInvocable(attributes, report); this.validateUserInvokable(attributes, report); this.validateDisableModelInvocation(attributes, report); + this.validateAutoApprove(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, target, report); if (isVSCodeOrDefaultTarget(target)) { this.validateModel(attributes, ChatModeKind.Agent, report); @@ -659,6 +660,17 @@ export class PromptValidator { } } + private validateAutoApprove(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.autoApprove); + if (!attribute) { + return; + } + if (!isTrueOrFalse(attribute.value)) { + report(toMarker(localize('promptValidator.autoApproveMustBeBoolean', "The 'auto-approve' attribute must be 'true' or 'false'."), attribute.value.range, MarkerSeverity.Error)); + return; + } + } + private async validateAgentsAttribute(attributes: IHeaderAttribute[], header: PromptHeader, report: (markers: IMarkerData) => void): Promise { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.agents); if (!attribute) { From 06e406bab614018c6fb11ab313103ebc5ad3990a Mon Sep 17 00:00:00 2001 From: justschen Date: Fri, 20 Feb 2026 23:06:52 -0800 Subject: [PATCH 06/25] make some tests --- .../promptHeaderAutocompletion.test.ts | 1 + .../languageProviders/promptValidator.test.ts | 2 +- .../tools/languageModelToolsService.test.ts | 194 +++++++++++++++++- .../chat/test/common/chatModeService.test.ts | 21 +- .../service/promptsService.test.ts | 14 ++ 5 files changed, 226 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index fe90e7a434e45..2dc6f242f3037 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -140,6 +140,7 @@ suite('PromptHeaderAutocompletion', () => { assert.deepStrictEqual(actual.sort(sortByLabel), [ { label: 'agents', result: 'agents: ${0:["*"]}' }, { label: 'argument-hint', result: 'argument-hint: $0' }, + { label: 'auto-approve', result: 'auto-approve: $0' }, { label: 'disable-model-invocation', result: 'disable-model-invocation: ${0:true}' }, { label: 'handoffs', result: 'handoffs: $0' }, { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index a552eb7855633..e0be1731050d4 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -551,7 +551,7 @@ suite('PromptValidator', () => { assert.deepStrictEqual( markers.map(m => ({ severity: m.severity, message: m.message })), [ - { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, handoffs, model, name, target, tools, user-invocable.` }, + { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, auto-approve, description, disable-model-invocation, handoffs, model, name, target, tools, user-invocable.` }, ] ); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index f642745892e92..714acd550d525 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -22,9 +22,9 @@ import { ExtensionIdentifier } from '../../../../../../platform/extensions/commo import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../../browser/tools/languageModelToolsService.js'; -import { ChatModel, IChatModel } from '../../../common/model/chatModel.js'; +import { ChatModel, IChatModel, IChatRequestModeInfo } from '../../../common/model/chatModel.js'; import { IChatService, IChatToolInputInvocationData, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; +import { ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; import { SpecedToolAliases, isToolResultInputOutputDetails, IToolData, IToolImpl, IToolInvocation, ToolDataSource, ToolSet, IToolResultTextPart } from '../../../common/tools/languageModelToolsService.js'; import { MockChatService } from '../../common/chatService/mockChatService.js'; import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -83,13 +83,14 @@ function registerToolForTest(service: LanguageModelToolsService, store: any, id: }; } -function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any } }): IChatModel { +function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any }; modeInfo?: IChatRequestModeInfo }): IChatModel { const requestId = options?.requestId ?? 'requestId'; const capture = options?.capture; + const modeInfo = options?.modeInfo; const fakeModel = { sessionId, sessionResource: LocalChatSessionUri.forSession(sessionId), - getRequests: () => [{ id: requestId, modelId: 'test-model' }], + getRequests: () => [{ id: requestId, modelId: 'test-model', modeInfo }], } as ChatModel; chatService.addSession(fakeModel); chatService.appendProgress = (request, progress) => { @@ -4214,4 +4215,189 @@ suite('LanguageModelToolsService', () => { assert.deepStrictEqual(receivedParameters, { command: 'safe-command' }); }); }); + + suite('Autopilot mode and YOLO', () => { + + test('autopilot mode auto-approves tool calls', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store); + + const tool = registerToolForTest(testService, store, 'autopilotTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved by autopilot' }] }) + }); + + const sessionId = 'autopilot-session'; + stubGetSession(testChatService, sessionId, { + requestId: 'req-autopilot', + modeInfo: { + kind: ChatModeKind.Autopilot, + isBuiltin: true, + modeInstructions: undefined, + modeId: 'autopilot', + applyCodeBlockSuggestionId: undefined, + autoApprove: true, + }, + }); + + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'auto approved by autopilot'); + }); + + test('autopilot mode does not auto-approve toolIdThatCannotBeAutoApproved', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store); + + const capture: { invocation?: any } = {}; + const tool = registerToolForTest(testService, store, 'vscode_get_confirmation_with_options', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm', message: 'Must confirm' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'confirmed' }] }) + }); + + const sessionId = 'autopilot-blocked'; + stubGetSession(testChatService, sessionId, { + requestId: 'req-blocked', + capture, + modeInfo: { + kind: ChatModeKind.Autopilot, + isBuiltin: true, + modeInstructions: undefined, + modeId: 'autopilot', + applyCodeBlockSuggestionId: undefined, + autoApprove: true, + }, + }); + + const resultPromise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'vscode_get_confirmation_with_options should still require confirmation in autopilot mode'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await resultPromise; + assert.strictEqual(result.content[0].value, 'confirmed'); + }); + + test('session YOLO mode auto-approves tool calls', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store); + + const tool = registerToolForTest(testService, store, 'yoloTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'yolo approved' }] }) + }); + + const sessionId = 'yolo-session'; + stubGetSession(testChatService, sessionId, { requestId: 'req-yolo' }); + + const sessionResource = LocalChatSessionUri.forSession(sessionId); + testService.enableSessionYolo(sessionResource); + + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'yolo approved'); + }); + + test('session YOLO can be disabled', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store); + + const capture: { invocation?: any } = {}; + const tool = registerToolForTest(testService, store, 'yoloToggleTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'done' }] }) + }); + + const sessionId = 'yolo-toggle'; + stubGetSession(testChatService, sessionId, { requestId: 'req-toggle', capture }); + + const sessionResource = LocalChatSessionUri.forSession(sessionId); + testService.enableSessionYolo(sessionResource); + assert.ok(testService.isSessionYolo(sessionResource)); + + testService.disableSessionYolo(sessionResource); + assert.ok(!testService.isSessionYolo(sessionResource)); + + // After disabling, tool should require confirmation + const resultPromise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should require confirmation after YOLO is disabled'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await resultPromise; + assert.strictEqual(result.content[0].value, 'done'); + }); + + test('next-request YOLO auto-approves only for the consuming request', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store); + + const tool = registerToolForTest(testService, store, 'nextReqTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'next-req approved' }] }) + }); + + const sessionId = 'next-req-session'; + stubGetSession(testChatService, sessionId, { requestId: 'req-next' }); + + const sessionResource = LocalChatSessionUri.forSession(sessionId); + testService.enableNextRequestYolo(sessionResource); + + // First invocation should auto-approve (consumes the flag) + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'next-req approved'); + }); + + test('agent mode without autoApprove requires confirmation', async () => { + const { service: testService, chatService: testChatService } = createTestToolsService(store); + + const capture: { invocation?: any } = {}; + const tool = registerToolForTest(testService, store, 'agentTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'agent done' }] }) + }); + + const sessionId = 'agent-no-autoapprove'; + stubGetSession(testChatService, sessionId, { + requestId: 'req-agent', + capture, + modeInfo: { + kind: ChatModeKind.Agent, + isBuiltin: true, + modeInstructions: undefined, + modeId: 'agent', + applyCodeBlockSuggestionId: undefined, + // no autoApprove — normal agent mode + }, + }); + + const resultPromise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'regular agent mode should require confirmation'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await resultPromise; + assert.strictEqual(result.content[0].value, 'agent done'); + }); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts index f7745a0b6fa9f..9f69f95a56c51 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -72,7 +72,7 @@ suite('ChatModeService', () => { test('should return builtin modes', () => { const modes = chatModeService.getModes(); - assert.strictEqual(modes.builtin.length, 3); + assert.strictEqual(modes.builtin.length, 4); assert.strictEqual(modes.custom.length, 0); // Check that Ask mode is always present @@ -83,6 +83,25 @@ suite('ChatModeService', () => { assert.strictEqual(askMode.kind, ChatModeKind.Ask); }); + test('should include Autopilot mode with autoApprove', () => { + const modes = chatModeService.getModes(); + + const autopilotMode = modes.builtin.find(mode => mode.id === ChatModeKind.Autopilot); + assert.ok(autopilotMode, 'Autopilot mode should be present'); + assert.strictEqual(autopilotMode.label.get(), 'Autopilot'); + assert.strictEqual(autopilotMode.kind, ChatModeKind.Autopilot); + assert.strictEqual(autopilotMode.autoApprove?.get(), true, 'Autopilot should have autoApprove enabled'); + }); + + test('Autopilot mode should be positioned after Agent mode', () => { + const modes = chatModeService.getModes(); + const agentIndex = modes.builtin.findIndex(mode => mode.id === ChatModeKind.Agent); + const autopilotIndex = modes.builtin.findIndex(mode => mode.id === ChatModeKind.Autopilot); + assert.ok(agentIndex >= 0, 'Agent mode should exist'); + assert.ok(autopilotIndex >= 0, 'Autopilot mode should exist'); + assert.strictEqual(autopilotIndex, agentIndex + 1, 'Autopilot should be right after Agent'); + }); + test('should adjust builtin modes based on tools agent availability', () => { // Agent mode should always be present regardless of tools agent availability chatAgentService.setHasToolsAgent(true); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 1efdafbe4de61..86bcd5613cbb6 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -789,6 +789,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -845,6 +846,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local }, }, @@ -920,6 +922,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -938,6 +941,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1008,6 +1012,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1026,6 +1031,7 @@ suite('PromptsService', () => { tools: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1044,6 +1050,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1121,6 +1128,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/copilot-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1141,6 +1149,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent.md'), source: { storage: PromptsStorage.local } }, @@ -1160,6 +1169,7 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent2.md'), source: { storage: PromptsStorage.local } }, @@ -1215,6 +1225,7 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'), source: { storage: PromptsStorage.local } } @@ -1285,6 +1296,7 @@ suite('PromptsService', () => { argumentHint: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/restricted-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1303,6 +1315,7 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/no-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1321,6 +1334,7 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, + autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/full-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, From 90eafa87e8882a8d433cf241662d9be15cf92adf Mon Sep 17 00:00:00 2001 From: justschen Date: Sat, 21 Feb 2026 01:45:55 -0800 Subject: [PATCH 07/25] fix tests --- .../tools/languageModelToolsService.test.ts | 79 ------------------- 1 file changed, 79 deletions(-) diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index 714acd550d525..2d20d315a9451 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -4284,85 +4284,6 @@ suite('LanguageModelToolsService', () => { assert.strictEqual(result.content[0].value, 'confirmed'); }); - test('session YOLO mode auto-approves tool calls', async () => { - const { service: testService, chatService: testChatService } = createTestToolsService(store); - - const tool = registerToolForTest(testService, store, 'yoloTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'yolo approved' }] }) - }); - - const sessionId = 'yolo-session'; - stubGetSession(testChatService, sessionId, { requestId: 'req-yolo' }); - - const sessionResource = LocalChatSessionUri.forSession(sessionId); - testService.enableSessionYolo(sessionResource); - - const result = await testService.invokeTool( - tool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - assert.strictEqual(result.content[0].value, 'yolo approved'); - }); - - test('session YOLO can be disabled', async () => { - const { service: testService, chatService: testChatService } = createTestToolsService(store); - - const capture: { invocation?: any } = {}; - const tool = registerToolForTest(testService, store, 'yoloToggleTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'done' }] }) - }); - - const sessionId = 'yolo-toggle'; - stubGetSession(testChatService, sessionId, { requestId: 'req-toggle', capture }); - - const sessionResource = LocalChatSessionUri.forSession(sessionId); - testService.enableSessionYolo(sessionResource); - assert.ok(testService.isSessionYolo(sessionResource)); - - testService.disableSessionYolo(sessionResource); - assert.ok(!testService.isSessionYolo(sessionResource)); - - // After disabling, tool should require confirmation - const resultPromise = testService.invokeTool( - tool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - - const published = await waitForPublishedInvocation(capture); - assert.ok(published?.confirmationMessages, 'tool should require confirmation after YOLO is disabled'); - - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await resultPromise; - assert.strictEqual(result.content[0].value, 'done'); - }); - - test('next-request YOLO auto-approves only for the consuming request', async () => { - const { service: testService, chatService: testChatService } = createTestToolsService(store); - - const tool = registerToolForTest(testService, store, 'nextReqTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'next-req approved' }] }) - }); - - const sessionId = 'next-req-session'; - stubGetSession(testChatService, sessionId, { requestId: 'req-next' }); - - const sessionResource = LocalChatSessionUri.forSession(sessionId); - testService.enableNextRequestYolo(sessionResource); - - // First invocation should auto-approve (consumes the flag) - const result = await testService.invokeTool( - tool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - assert.strictEqual(result.content[0].value, 'next-req approved'); - }); - test('agent mode without autoApprove requires confirmation', async () => { const { service: testService, chatService: testChatService } = createTestToolsService(store); From cad63e2d74e771aa5d6d1061a2a0b95a8b9080b7 Mon Sep 17 00:00:00 2001 From: justschen Date: Tue, 24 Feb 2026 14:01:59 -0800 Subject: [PATCH 08/25] some reverts to cleaner state --- .../chat/browser/actions/chatActions.ts | 19 --- .../contrib/chat/browser/chatSlashCommands.ts | 27 ---- .../tools/languageModelToolsService.ts | 107 ---------------- .../browser/widget/input/chatInputPart.ts | 1 - .../contrib/chat/common/chatModes.ts | 19 +-- .../contrib/chat/common/model/chatModel.ts | 1 - .../languageProviders/promptValidator.ts | 16 --- .../common/promptSyntax/promptFileParser.ts | 5 - .../promptSyntax/service/promptsService.ts | 5 - .../service/promptsServiceImpl.ts | 3 +- .../common/tools/languageModelToolsService.ts | 21 ---- .../promptHeaderAutocompletion.test.ts | 1 - .../languageProviders/promptValidator.test.ts | 4 - .../tools/languageModelToolsService.test.ts | 115 +----------------- .../chat/test/common/chatModeService.test.ts | 3 +- .../service/promptsService.test.ts | 14 --- .../tools/mockLanguageModelToolsService.ts | 10 -- 17 files changed, 8 insertions(+), 363 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 3c83acc19f78b..4903908dd4997 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1031,25 +1031,6 @@ export function registerChatActions() { } }); - registerAction2(class ToggleYoloModeAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.chat.toggleYoloMode', - title: localize2('chat.toggleYoloMode', "Toggle YOLO Mode"), - category: CHAT_CATEGORY, - f1: true, - precondition: ChatContextKeys.enabled, - toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.GlobalAutoApprove}`, true), - }); - } - - async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - const currentValue = configurationService.getValue(ChatConfiguration.GlobalAutoApprove); - await configurationService.updateValue(ChatConfiguration.GlobalAutoApprove, !currentValue); - } - }); - const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id)); registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts index a0b0b78347181..7e1830cee3505 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSlashCommands.ts @@ -221,33 +221,6 @@ export class ChatSlashCommandsContribution extends Disposable { silent: true, locations: [ChatAgentLocation.Chat] }, handleAutoApprove)); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'yolo', - detail: nls.localize('yolo', "Toggle auto-approval of all tool calls"), - sortText: 'z1_yolo', - executeImmediately: false, - silent: true, - locations: [ChatAgentLocation.Chat] - }, async (prompt, _progress, _history, _location, sessionResource) => { - const trimmed = prompt.trim(); - if (trimmed) { - // /yolo — enable for next request only, then submit - toolsService.enableNextRequestYolo(sessionResource); - const widget = chatWidgetService.getWidgetBySessionResource(sessionResource); - if (widget) { - widget.acceptInput(trimmed); - } - } else { - // /yolo — toggle session YOLO mode - if (toolsService.isSessionYolo(sessionResource)) { - toolsService.disableSessionYolo(sessionResource); - notificationService.info(nls.localize('yolo.disabled', "YOLO mode disabled for this session")); - } else { - toolsService.enableSessionYolo(sessionResource); - notificationService.info(nls.localize('yolo.enabled', "YOLO mode enabled for this session — all tools will be auto-approved")); - } - } - })); this._store.add(slashCommandService.registerSlashCommand({ command: 'help', detail: '', diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index f70bb3e04b7a4..a367b84a045b7 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -110,13 +110,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo /** Pending tool calls in the streaming phase, keyed by toolCallId */ private readonly _pendingToolCalls = new Map(); - /** Sessions with YOLO mode enabled (keyed by session resource toString) */ - private readonly _yoloSessions = new Set(); - - /** Sessions with YOLO mode enabled for only the next request (keyed by session resource toString) */ - /** Maps session key → request ID that consumed the flag (null = pending, not yet consumed) */ - private readonly _yoloNextRequestSessions = new Map(); - private readonly _isAgentModeEnabled: IObservable; constructor( @@ -205,7 +198,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo description: localize('copilot.toolSet.agent.description', 'Delegate tasks to other agents'), } )); - } /** @@ -253,42 +245,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return false; } - private _yoloCleanupRegistered = false; - - private _ensureYoloCleanup(): void { - if (this._yoloCleanupRegistered) { - return; - } - this._yoloCleanupRegistered = true; - - // Clean up YOLO state when sessions are disposed - this._register(this._chatService.onDidDisposeSession(e => { - for (const sessionResource of e.sessionResource) { - const key = sessionResource.toString(); - this._yoloSessions.delete(key); - this._yoloNextRequestSessions.delete(key); - } - })); - } - - enableSessionYolo(sessionResource: URI): void { - this._ensureYoloCleanup(); - this._yoloSessions.add(sessionResource.toString()); - } - - disableSessionYolo(sessionResource: URI): void { - this._yoloSessions.delete(sessionResource.toString()); - } - - isSessionYolo(sessionResource: URI): boolean { - return this._yoloSessions.has(sessionResource.toString()); - } - - enableNextRequestYolo(sessionResource: URI): void { - this._ensureYoloCleanup(); - this._yoloNextRequestSessions.set(sessionResource.toString(), null); - } - override dispose(): void { super.dispose(); @@ -1087,42 +1043,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - // This tool always requires user confirmation (pre-agentic-loop options). - // Not even YOLO mode should bypass it. - if (toolId === toolIdThatCannotBeAutoApproved) { - return undefined; - } - - // Session-scoped YOLO mode (via /yolo slash command) bypasses per-tool - // eligibility settings — the user explicitly asked to auto-approve everything. - if (chatSessionResource) { - const key = chatSessionResource.toString(); - if (this._yoloSessions.has(key)) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; - } - - if (this._yoloNextRequestSessions.has(key)) { - const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); - const consumedBy = this._yoloNextRequestSessions.get(key); - if (consumedBy === null && request) { - this._yoloNextRequestSessions.set(key, request.id); - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; - } else if (consumedBy !== null && consumedBy === request?.id) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; - } else { - this._yoloNextRequestSessions.delete(key); - } - } - - // Mode-level auto-approve (e.g. autopilot agent with auto-approve: true) - const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); - if (request?.modeInfo?.autoApprove) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'mode-auto-approve' }; - } - } - if (!this.isToolEligibleForAutoApproval(tool.data)) { return undefined; } @@ -1155,33 +1075,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { - if (toolId === toolIdThatCannotBeAutoApproved) { - return undefined; - } - - // Session-scoped YOLO mode bypasses all post-execution confirmations - if (chatSessionResource) { - const key = chatSessionResource.toString(); - if (this._yoloSessions.has(key)) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; - } - - const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); - - if (this._yoloNextRequestSessions.has(key)) { - const consumedBy = this._yoloNextRequestSessions.get(key); - if (consumedBy !== null && consumedBy === request?.id) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'session-yolo' }; - } - } - - // Mode-level auto-approve - if (request?.modeInfo?.autoApprove) { - return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'mode-auto-approve' }; - } - } - if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index dde0e1e107ce4..ec440a7c588d4 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -419,7 +419,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, - autoApprove: mode.autoApprove?.get(), }; } diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 6b8190fe052f5..7b1805079a7e0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -129,7 +129,7 @@ export class ChatModeService extends Disposable implements IChatModeService { target: cachedMode.target ?? Target.Undefined, visibility, agents: cachedMode.agents, - autoApprove: cachedMode.autoApprove, + source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local } }; const instance = new CustomChatMode(customChatMode); @@ -259,7 +259,6 @@ export interface IChatModeData { readonly visibility?: ICustomAgentVisibility; readonly agents?: readonly string[]; readonly infer?: boolean; // deprecated, only available in old cached data - readonly autoApprove?: boolean; } export interface IChatMode { @@ -280,7 +279,6 @@ export interface IChatMode { readonly target: IObservable; readonly visibility?: IObservable; readonly agents?: IObservable; - readonly autoApprove?: IObservable; } export interface IVariableReference { @@ -328,7 +326,6 @@ export class CustomChatMode implements IChatMode { private readonly _targetObservable: ISettableObservable; private readonly _visibilityObservable: ISettableObservable; private readonly _agentsObservable: ISettableObservable; - private readonly _autoApproveObservable: ISettableObservable; private _source: IAgentSource; public readonly id: string; @@ -393,10 +390,6 @@ export class CustomChatMode implements IChatMode { return this._agentsObservable; } - get autoApprove(): IObservable { - return this._autoApproveObservable; - } - public readonly kind = ChatModeKind.Agent; constructor( @@ -412,7 +405,6 @@ export class CustomChatMode implements IChatMode { this._targetObservable = observableValue('target', customChatMode.target); this._visibilityObservable = observableValue('visibility', customChatMode.visibility); this._agentsObservable = observableValue('agents', customChatMode.agents); - this._autoApproveObservable = observableValue('autoApprove', customChatMode.autoApprove); this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions); this._uriObservable = observableValue('uri', customChatMode.uri); this._source = customChatMode.source; @@ -432,7 +424,6 @@ export class CustomChatMode implements IChatMode { this._targetObservable.set(newData.target, tx); this._visibilityObservable.set(newData.visibility, tx); this._agentsObservable.set(newData.agents, tx); - this._autoApproveObservable.set(newData.autoApprove, tx); this._modeInstructions.set(newData.agentInstructions, tx); this._uriObservable.set(newData.uri, tx); this._source = newData.source; @@ -455,7 +446,6 @@ export class CustomChatMode implements IChatMode { target: this.target.get(), visibility: this.visibility.get(), agents: this.agents.get(), - autoApprove: this.autoApprove.get(), }; } } @@ -501,23 +491,18 @@ export class BuiltinChatMode implements IChatMode { public readonly description: IObservable; public readonly icon: IObservable; public readonly target: IObservable; - public readonly autoApprove?: IObservable; constructor( public readonly kind: ChatModeKind, label: string, description: string, icon: ThemeIcon, - options?: { autoApprove?: boolean }, ) { this.name = constObservable(kind); this.label = constObservable(label); this.description = observableValue('description', description); this.icon = constObservable(icon); this.target = constObservable(Target.Undefined); - if (options?.autoApprove) { - this.autoApprove = constObservable(true); - } } public get isBuiltin(): boolean { @@ -546,7 +531,7 @@ export namespace ChatMode { export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question); export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit); export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent); - export const Autopilot = new BuiltinChatMode(ChatModeKind.Autopilot, 'Autopilot', localize('autopilotDescription', "Auto-approve all tools and run autonomously"), Codicon.rocket, { autoApprove: true }); + export const Autopilot = new BuiltinChatMode(ChatModeKind.Autopilot, 'Autopilot', localize('autopilotDescription', "Auto-approve all tools and run autonomously"), Codicon.rocket); } export function isBuiltinChatMode(mode: IChatMode): boolean { diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index f3c1781504e2a..6f14c3ef529e5 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -314,7 +314,6 @@ export interface IChatRequestModeInfo { modeInstructions: IChatRequestModeInstructions | undefined; modeId: 'ask' | 'agent' | 'edit' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; - autoApprove?: boolean; } export interface IChatRequestModeInstructions { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 00f7bf02bf2e3..a6058e2c47ec7 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -189,7 +189,6 @@ export class PromptValidator { this.validateUserInvocable(attributes, report); this.validateUserInvokable(attributes, report); this.validateDisableModelInvocation(attributes, report); - this.validateAutoApprove(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, target, report); if (isVSCodeOrDefaultTarget(target)) { this.validateModel(attributes, ChatModeKind.Agent, report); @@ -663,17 +662,6 @@ export class PromptValidator { } } - private validateAutoApprove(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { - const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.autoApprove); - if (!attribute) { - return; - } - if (!isTrueOrFalse(attribute.value)) { - report(toMarker(localize('promptValidator.autoApproveMustBeBoolean', "The 'auto-approve' attribute must be 'true' or 'false'."), attribute.value.range, MarkerSeverity.Error)); - return; - } - } - private async validateAgentsAttribute(attributes: IHeaderAttribute[], header: PromptHeader, report: (markers: IMarkerData) => void): Promise { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.agents); if (!attribute) { @@ -772,11 +760,7 @@ function isTrueOrFalse(value: IValue): boolean { const allAttributeNames: Record = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], -<<<<<<< justin/whimsicott - [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, PromptHeaderAttributes.autoApprove], -======= [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation, GithubPromptHeaderAttributes.github], ->>>>>>> main [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.userInvocable, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation], [PromptsType.hook]: [], // hooks are JSON files, not markdown with YAML frontmatter }; diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index add1ef30999b3..f6b160c7ee276 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -85,7 +85,6 @@ export namespace PromptHeaderAttributes { export const userInvokable = 'user-invokable'; export const userInvocable = 'user-invocable'; export const disableModelInvocation = 'disable-model-invocation'; - export const autoApprove = 'auto-approve'; } export namespace GithubPromptHeaderAttributes { @@ -332,10 +331,6 @@ export class PromptHeader { return this.getBooleanAttribute(PromptHeaderAttributes.disableModelInvocation); } - public get autoApprove(): boolean | undefined { - return this.getBooleanAttribute(PromptHeaderAttributes.autoApprove); - } - private getBooleanAttribute(key: string): boolean | undefined { const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); if (attribute?.value.type === 'scalar') { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts index f33dcea5e61d3..23113daea69c1 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -223,11 +223,6 @@ export interface ICustomAgent { */ readonly agents?: readonly string[]; - /** - * When true, all tool calls are auto-approved (YOLO mode) for this agent. - */ - readonly autoApprove?: boolean; - /** * Where the agent was loaded from. */ diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 33ff8d965378f..e42b4fde1f921 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -719,8 +719,7 @@ export class PromptsService extends Disposable implements IPromptsService { if (target === Target.Claude && tools) { tools = mapClaudeTools(tools); } - const autoApprove = ast.header.autoApprove; - return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, autoApprove, agentInstructions, source }; + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source }; }) ); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts index a55f006114bb9..d14a93696e08e 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -586,27 +586,6 @@ export interface ILanguageModelToolsService { toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[]; toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; - - /** - * Enable YOLO mode (auto-approve all tools) for the given chat session. - */ - enableSessionYolo(sessionResource: URI): void; - - /** - * Disable YOLO mode for the given chat session. - */ - disableSessionYolo(sessionResource: URI): void; - - /** - * Check whether YOLO mode is active for the given chat session. - */ - isSessionYolo(sessionResource: URI): boolean; - - /** - * Enable YOLO mode for just the next request in the given chat session. - * The flag is automatically cleared when the request completes. - */ - enableNextRequestYolo(sessionResource: URI): void; } diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts index cd9026b09c619..8fdd981ea7186 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -140,7 +140,6 @@ suite('PromptHeaderAutocompletion', () => { assert.deepStrictEqual(actual.sort(sortByLabel), [ { label: 'agents', result: 'agents: ${0:["*"]}' }, { label: 'argument-hint', result: 'argument-hint: $0' }, - { label: 'auto-approve', result: 'auto-approve: $0' }, { label: 'disable-model-invocation', result: 'disable-model-invocation: ${0:true}' }, { label: 'github', result: 'github: $0' }, { label: 'handoffs', result: 'handoffs: $0' }, diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts index 51c1e449f7f1d..e3cc440de6346 100644 --- a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -551,11 +551,7 @@ suite('PromptValidator', () => { assert.deepStrictEqual( markers.map(m => ({ severity: m.severity, message: m.message })), [ -<<<<<<< justin/whimsicott - { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, auto-approve, description, disable-model-invocation, handoffs, model, name, target, tools, user-invocable.` }, -======= { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, github, handoffs, model, name, target, tools, user-invocable.` }, ->>>>>>> main ] ); }); diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts index 2d20d315a9451..f642745892e92 100644 --- a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -22,9 +22,9 @@ import { ExtensionIdentifier } from '../../../../../../platform/extensions/commo import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; import { LanguageModelToolsService } from '../../../browser/tools/languageModelToolsService.js'; -import { ChatModel, IChatModel, IChatRequestModeInfo } from '../../../common/model/chatModel.js'; +import { ChatModel, IChatModel } from '../../../common/model/chatModel.js'; import { IChatService, IChatToolInputInvocationData, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; -import { ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; +import { ChatConfiguration } from '../../../common/constants.js'; import { SpecedToolAliases, isToolResultInputOutputDetails, IToolData, IToolImpl, IToolInvocation, ToolDataSource, ToolSet, IToolResultTextPart } from '../../../common/tools/languageModelToolsService.js'; import { MockChatService } from '../../common/chatService/mockChatService.js'; import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -83,14 +83,13 @@ function registerToolForTest(service: LanguageModelToolsService, store: any, id: }; } -function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any }; modeInfo?: IChatRequestModeInfo }): IChatModel { +function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any } }): IChatModel { const requestId = options?.requestId ?? 'requestId'; const capture = options?.capture; - const modeInfo = options?.modeInfo; const fakeModel = { sessionId, sessionResource: LocalChatSessionUri.forSession(sessionId), - getRequests: () => [{ id: requestId, modelId: 'test-model', modeInfo }], + getRequests: () => [{ id: requestId, modelId: 'test-model' }], } as ChatModel; chatService.addSession(fakeModel); chatService.appendProgress = (request, progress) => { @@ -4215,110 +4214,4 @@ suite('LanguageModelToolsService', () => { assert.deepStrictEqual(receivedParameters, { command: 'safe-command' }); }); }); - - suite('Autopilot mode and YOLO', () => { - - test('autopilot mode auto-approves tool calls', async () => { - const { service: testService, chatService: testChatService } = createTestToolsService(store); - - const tool = registerToolForTest(testService, store, 'autopilotTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved by autopilot' }] }) - }); - - const sessionId = 'autopilot-session'; - stubGetSession(testChatService, sessionId, { - requestId: 'req-autopilot', - modeInfo: { - kind: ChatModeKind.Autopilot, - isBuiltin: true, - modeInstructions: undefined, - modeId: 'autopilot', - applyCodeBlockSuggestionId: undefined, - autoApprove: true, - }, - }); - - const result = await testService.invokeTool( - tool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - assert.strictEqual(result.content[0].value, 'auto approved by autopilot'); - }); - - test('autopilot mode does not auto-approve toolIdThatCannotBeAutoApproved', async () => { - const { service: testService, chatService: testChatService } = createTestToolsService(store); - - const capture: { invocation?: any } = {}; - const tool = registerToolForTest(testService, store, 'vscode_get_confirmation_with_options', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm', message: 'Must confirm' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'confirmed' }] }) - }); - - const sessionId = 'autopilot-blocked'; - stubGetSession(testChatService, sessionId, { - requestId: 'req-blocked', - capture, - modeInfo: { - kind: ChatModeKind.Autopilot, - isBuiltin: true, - modeInstructions: undefined, - modeId: 'autopilot', - applyCodeBlockSuggestionId: undefined, - autoApprove: true, - }, - }); - - const resultPromise = testService.invokeTool( - tool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - - const published = await waitForPublishedInvocation(capture); - assert.ok(published?.confirmationMessages, 'vscode_get_confirmation_with_options should still require confirmation in autopilot mode'); - - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await resultPromise; - assert.strictEqual(result.content[0].value, 'confirmed'); - }); - - test('agent mode without autoApprove requires confirmation', async () => { - const { service: testService, chatService: testChatService } = createTestToolsService(store); - - const capture: { invocation?: any } = {}; - const tool = registerToolForTest(testService, store, 'agentTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Needs approval' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'agent done' }] }) - }); - - const sessionId = 'agent-no-autoapprove'; - stubGetSession(testChatService, sessionId, { - requestId: 'req-agent', - capture, - modeInfo: { - kind: ChatModeKind.Agent, - isBuiltin: true, - modeInstructions: undefined, - modeId: 'agent', - applyCodeBlockSuggestionId: undefined, - // no autoApprove — normal agent mode - }, - }); - - const resultPromise = testService.invokeTool( - tool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - - const published = await waitForPublishedInvocation(capture); - assert.ok(published?.confirmationMessages, 'regular agent mode should require confirmation'); - - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await resultPromise; - assert.strictEqual(result.content[0].value, 'agent done'); - }); - }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts index 9f69f95a56c51..c417003b6afa0 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -83,14 +83,13 @@ suite('ChatModeService', () => { assert.strictEqual(askMode.kind, ChatModeKind.Ask); }); - test('should include Autopilot mode with autoApprove', () => { + test('should include Autopilot mode', () => { const modes = chatModeService.getModes(); const autopilotMode = modes.builtin.find(mode => mode.id === ChatModeKind.Autopilot); assert.ok(autopilotMode, 'Autopilot mode should be present'); assert.strictEqual(autopilotMode.label.get(), 'Autopilot'); assert.strictEqual(autopilotMode.kind, ChatModeKind.Autopilot); - assert.strictEqual(autopilotMode.autoApprove?.get(), true, 'Autopilot should have autoApprove enabled'); }); test('Autopilot mode should be positioned after Agent mode', () => { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index e1ab58240e107..5a784486c3200 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -794,7 +794,6 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -851,7 +850,6 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local }, }, @@ -927,7 +925,6 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -946,7 +943,6 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1017,7 +1013,6 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1036,7 +1031,6 @@ suite('PromptsService', () => { tools: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1055,7 +1049,6 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1133,7 +1126,6 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/copilot-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1154,7 +1146,6 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent.md'), source: { storage: PromptsStorage.local } }, @@ -1174,7 +1165,6 @@ suite('PromptsService', () => { argumentHint: undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.claude/agents/claude-agent2.md'), source: { storage: PromptsStorage.local } }, @@ -1230,7 +1220,6 @@ suite('PromptsService', () => { target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, agents: undefined, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'), source: { storage: PromptsStorage.local } } @@ -1301,7 +1290,6 @@ suite('PromptsService', () => { argumentHint: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/restricted-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1320,7 +1308,6 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/no-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1339,7 +1326,6 @@ suite('PromptsService', () => { tools: undefined, target: Target.Undefined, visibility: { userInvocable: true, agentInvocable: true }, - autoApprove: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/full-access-agent.agent.md'), source: { storage: PromptsStorage.local } }, diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts index 4182612968690..7d1ca3200bca2 100644 --- a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -186,14 +186,4 @@ export class MockLanguageModelToolsService extends Disposable implements ILangua getDeprecatedFullReferenceNames(): Map> { throw new Error('Method not implemented.'); } - - enableSessionYolo(_sessionResource: URI): void { } - - disableSessionYolo(_sessionResource: URI): void { } - - isSessionYolo(_sessionResource: URI): boolean { - return false; - } - - enableNextRequestYolo(_sessionResource: URI): void { } } From 7bd80e7877b3b4262c6aa35ce8de4d3f4cf60773 Mon Sep 17 00:00:00 2001 From: justschen Date: Wed, 25 Feb 2026 12:05:20 -0800 Subject: [PATCH 09/25] add secondary toolbar, permissions --- src/vs/platform/actions/common/actions.ts | 1 + .../browser/actions/chatExecuteActions.ts | 45 ++++++- .../chatEditing/chatEditingSessionStorage.ts | 2 +- .../tools/languageModelToolsService.ts | 21 ++- .../browser/widget/input/chatInputPart.ts | 127 ++++++++++++------ .../widget/input/modePickerActionItem.ts | 1 - .../input/permissionPickerActionItem.ts | 106 +++++++++++++++ .../chat/browser/widget/media/chat.css | 71 ++++++++++ .../chat/common/actions/chatContextKeys.ts | 4 +- .../contrib/chat/common/chatModes.ts | 5 +- .../common/chatService/chatServiceImpl.ts | 42 +++++- .../contrib/chat/common/constants.ts | 14 +- .../chat/common/editing/chatEditingService.ts | 2 +- .../contrib/chat/common/model/chatModel.ts | 5 +- .../chat/common/participants/chatAgents.ts | 4 +- .../chat/test/common/chatModeService.test.ts | 20 +-- .../aiEditTelemetry/aiEditTelemetryService.ts | 2 - .../aiEditTelemetryServiceImpl.ts | 4 +- .../contrib/mcp/browser/mcpCommands.ts | 5 +- 19 files changed, 383 insertions(+), 98 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1b3e9d595c887..6a6019b9dca9c 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -255,6 +255,7 @@ export class MenuId { static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteQueue = new MenuId('ChatExecuteQueue'); static readonly ChatInput = new MenuId('ChatInput'); + static readonly ChatInputSecondary = new MenuId('ChatInputSecondary'); static readonly ChatInputSide = new MenuId('ChatInputSide'); static readonly ChatModePicker = new MenuId('ChatModePicker'); static readonly ChatEditingWidgetToolbar = new MenuId('ChatEditingWidgetToolbar'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 7e505aa90765c..4e813580465ba 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -26,7 +26,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { getModeNameForTelemetry, IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatVariableLeader } from '../../common/requestParser/chatParserTypes.js'; import { ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatService } from '../../common/chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; import { isInClaudeAgentsFolder } from '../../common/promptSyntax/config/promptFileLocations.js'; @@ -440,6 +440,42 @@ export class OpenModelPickerAction extends Action2 { } } } + +export class OpenPermissionPickerAction extends Action2 { + static readonly ID = 'workbench.action.chat.openPermissionPicker'; + + constructor() { + super({ + id: OpenPermissionPickerAction.ID, + title: localize2('interactive.openPermissionPicker.label', "Open Permission Picker"), + tooltip: localize('setPermissionLevel', "Set Permissions"), + category: CHAT_CATEGORY, + f1: false, + precondition: ChatContextKeys.enabled, + menu: { + id: MenuId.ChatInputSecondary, + order: 10, + group: 'navigation', + when: + ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask), + ChatContextKeys.inQuickChat.negate(), + ) + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const widgetService = accessor.get(IChatWidgetService); + const widget = widgetService.lastFocusedWidget; + if (widget) { + widget.input.openPermissionPicker(); + } + } +} + export class OpenModePickerAction extends Action2 { static readonly ID = 'workbench.action.chat.openModePicker'; @@ -501,7 +537,7 @@ export class OpenSessionTargetPickerAction extends Action2 { precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ { - id: MenuId.ChatInput, + id: MenuId.ChatInputSecondary, order: 0, when: ContextKeyExpr.and( ChatContextKeys.enabled, @@ -536,7 +572,7 @@ export class OpenDelegationPickerAction extends Action2 { precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ { - id: MenuId.ChatInput, + id: MenuId.ChatInputSecondary, order: 0.5, when: ContextKeyExpr.and( ChatContextKeys.enabled, @@ -571,7 +607,7 @@ export class OpenWorkspacePickerAction extends Action2 { precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.inAgentSessionsWelcome), menu: [ { - id: MenuId.ChatInput, + id: MenuId.ChatInputSecondary, order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, @@ -952,6 +988,7 @@ export function registerChatExecuteActions() { registerAction2(ToggleChatModeAction); registerAction2(SwitchToNextModelAction); registerAction2(OpenModelPickerAction); + registerAction2(OpenPermissionPickerAction); registerAction2(OpenModePickerAction); registerAction2(OpenSessionTargetPickerAction); registerAction2(OpenDelegationPickerAction); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts index 88816aa0df184..0b8e1e0eff253 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts @@ -267,7 +267,7 @@ interface IModifiedEntryTelemetryInfoDTO { readonly command?: string; readonly modelId?: string; - readonly modeId?: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; + readonly modeId?: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; readonly applyCodeBlockSuggestionId?: EditSuggestionId | undefined; readonly feature?: 'sideBarChat' | 'inlineChat' | undefined; } diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index a367b84a045b7..3ad3faff9ce33 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -40,7 +40,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { IVariableReference } from '../../common/chatModes.js'; import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; -import { ChatConfiguration } from '../../common/constants.js'; +import { ChatConfiguration, ChatPermissionLevel } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -1043,6 +1043,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } + // Auto-Approve All permission level bypasses all tool confirmations + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; + } + } + if (!this.isToolEligibleForAutoApproval(tool.data)) { return undefined; } @@ -1075,6 +1084,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + // Auto-Approve All permission level bypasses all post-execution confirmations + if (chatSessionResource) { + const model = this._chatService.getSession(chatSessionResource); + const request = model?.getRequests().at(-1); + if (request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; + } + } + if (this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove) && await this._checkGlobalAutoApprove()) { return { type: ToolConfirmKind.Setting, id: ChatConfiguration.GlobalAutoApprove }; } @@ -1150,7 +1168,6 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Clean up any pending tool calls that belong to this request for (const [toolCallId, invocation] of this._pendingToolCalls) { if (invocation.chatRequestId === requestId) { - invocation.cancelFromStreaming(ToolConfirmKind.Skipped); this._pendingToolCalls.delete(toolCallId); } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index ec440a7c588d4..bde6ebeb2532f 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -85,7 +85,7 @@ import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEnt import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; import { IChatFollowup, IChatQuestionCarousel, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, validateChatMode } from '../../../common/constants.js'; import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; @@ -95,7 +95,7 @@ import { IChatResponseViewModel, isResponseVM } from '../../../common/model/chat import { IChatAgentService } from '../../../common/participants/chatAgents.js'; import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; -import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; +import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenPermissionPickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; @@ -122,6 +122,7 @@ import { ChatSelectedTools } from './chatSelectedTools.js'; import { DelegationSessionPickerActionItem } from './delegationSessionPickerActionItem.js'; import { IModelPickerDelegate } from './modelPickerActionItem.js'; import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; +import { IPermissionPickerDelegate, PermissionPickerActionItem } from './permissionPickerActionItem.js'; import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; @@ -276,6 +277,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private container!: HTMLElement; private inputSideToolbarContainer?: HTMLElement; + private secondaryToolbarContainer!: HTMLElement; + private secondaryToolbar!: MenuWorkbenchToolBar; private followupsContainer!: HTMLElement; private readonly followupsDisposables: DisposableStore = this._register(new DisposableStore()); @@ -357,6 +360,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatSessionHasTargetedModels: IContextKey; private modelWidget: EnhancedModelPickerActionItem | undefined; private modeWidget: ModePickerActionItem | undefined; + private permissionWidget: PermissionPickerActionItem | undefined; private sessionTargetWidget: SessionTypePickerActionItem | undefined; private delegationWidget: DelegationSessionPickerActionItem | undefined; private chatSessionPickerWidgets: Map = new Map(); @@ -391,6 +395,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge readonly onDidChangeCurrentChatMode: Event = this._onDidChangeCurrentChatMode.event; private readonly _currentModeObservable: ISettableObservable; + private readonly _currentPermissionLevel: ISettableObservable; + private permissionLevelKey: IContextKey; public get currentModeKind(): ChatModeKind { const mode = this._currentModeObservable.get(); @@ -405,7 +411,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get currentModeInfo(): IChatRequestModeInfo { const mode = this._currentModeObservable.get(); - const modeId: 'ask' | 'agent' | 'edit' | 'autopilot' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; + const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; const modeInstructions = mode.modeInstructions?.get(); return { @@ -419,6 +425,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } : undefined, modeId: modeId, applyCodeBlockSuggestionId: undefined, + permissionLevel: this._currentPermissionLevel.get(), }; } @@ -519,6 +526,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); this._currentModeObservable = observableValue('currentMode', this.options.defaultMode ?? ChatMode.Agent); + this._currentPermissionLevel = observableValue('permissionLevel', ChatPermissionLevel.Default); this._register(this.editorService.onDidActiveEditorChange(() => { this._indexOfLastOpenedContext = -1; this.refreshChatSessionPickers(); @@ -568,6 +576,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService); this.chatModeNameKey = ChatContextKeys.chatModeName.bindTo(contextKeyService); this.chatModelIdKey = ChatContextKeys.chatModelId.bindTo(contextKeyService); + this.permissionLevelKey = ChatContextKeys.chatPermissionLevel.bindTo(contextKeyService); this.withinEditSessionKey = ChatContextKeys.withinEditSessionDiff.bindTo(contextKeyService); this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); this.chatSessionHasOptions = ChatContextKeys.chatSessionHasModels.bindTo(contextKeyService); @@ -788,6 +797,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.modeWidget?.show(); } + public openPermissionPicker(): void { + this.permissionWidget?.show(); + } + public openSessionTargetPicker(): void { this.sessionTargetWidget?.show(); } @@ -1945,6 +1958,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ]), ]), ]), + dom.h('.chat-secondary-toolbar@secondaryToolbar'), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attached-context@attachedContextContainer'), ]), @@ -1970,6 +1984,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ]), ]), ]), + dom.h('.chat-secondary-toolbar@secondaryToolbar'), ]); } this.container = elements.root; @@ -1990,6 +2005,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachmentsContainer = elements.attachmentsContainer; this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; + this.secondaryToolbarContainer = elements.secondaryToolbar; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatGettingStartedTipContainer = elements.chatGettingStartedTipContainer; @@ -2144,7 +2160,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge actionContext: { widget }, onlyShowIconsForDefaultActions: observableFromEvent( this._inputEditor.onDidLayoutChange, - (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 650 /* This is a magical number based on testing*/ + (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 450 /* Threshold for showing icon-only mode in primary toolbar pickers */ ).recomputeInitiallyAndOnChange(this._store), hoverPosition: { forcePosition: true, @@ -2196,40 +2212,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge }, }; return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions); - } else if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { - // Use provided delegate if available, otherwise create default delegate - const getActiveSessionType = () => { - const sessionResource = this._widget?.viewModel?.sessionResource; - return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; - }; - const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { - getActiveSessionProvider: () => { - return getActiveSessionType(); - }, - getPendingDelegationTarget: () => { - return this._pendingDelegationTarget; - }, - setPendingDelegationTarget: (provider: AgentSessionProviders) => { - const isActive = getActiveSessionType() === provider; - this._pendingDelegationTarget = isActive ? undefined : provider; - this.updateWidgetLockStateFromSessionType(provider); - this.updateAgentSessionTypeContextKey(); - this.refreshChatSessionPickers(); - }, - }; - const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; - const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; - return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); - } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { - if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { - return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); - } else { - const empty = new BaseActionViewItem(undefined, action); - if (empty.element) { - empty.element.style.display = 'none'; - } - return empty; - } } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { // Create all pickers and return a container action view item const widgets = this.createChatSessionPickerWidgets(action); @@ -2245,12 +2227,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar'); this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext; this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => { - // Update container reference for the pickers - const toolbarElement = this.inputActionsToolbar.getElement(); - // eslint-disable-next-line no-restricted-syntax - const container = toolbarElement.querySelector('.chat-sessionPicker-container'); - this.chatSessionPickerContainer = container as HTMLElement | undefined; - if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { this.layout(this.cachedWidth); } @@ -2283,6 +2259,69 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge toolbarSide.context = { widget } satisfies IChatExecuteActionContext; } + // Secondary toolbar (session type + permissions) — below the input box + this.secondaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.secondaryToolbarContainer, MenuId.ChatInputSecondary, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { shouldForwardArgs: true }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + hoverDelegate, + actionViewItemProvider: (action, options) => { + if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { + const getActiveSessionType = () => { + const sessionResource = this._widget?.viewModel?.sessionResource; + return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; + }; + const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { + getActiveSessionProvider: () => { + return getActiveSessionType(); + }, + getPendingDelegationTarget: () => { + return this._pendingDelegationTarget; + }, + setPendingDelegationTarget: (provider: AgentSessionProviders) => { + const isActive = getActiveSessionType() === provider; + this._pendingDelegationTarget = isActive ? undefined : provider; + this.updateWidgetLockStateFromSessionType(provider); + this.updateAgentSessionTypeContextKey(); + this.refreshChatSessionPickers(); + }, + }; + const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; + const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); + } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { + return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); + } else { + const empty = new BaseActionViewItem(undefined, action); + if (empty.element) { + empty.element.style.display = 'none'; + } + return empty; + } + } else if (action.id === OpenPermissionPickerAction.ID && action instanceof MenuItemAction) { + const delegate: IPermissionPickerDelegate = { + currentPermissionLevel: this._currentPermissionLevel, + setPermissionLevel: (level: ChatPermissionLevel) => { + this._currentPermissionLevel.set(level, undefined); + this.permissionLevelKey.set(level); + }, + }; + return this.permissionWidget = this.instantiationService.createInstance(PermissionPickerActionItem, action, delegate, pickerOptions); + } + return undefined; + } + })); + this.secondaryToolbar.getElement().classList.add('chat-secondary-input-toolbar'); + this.secondaryToolbar.context = { widget } satisfies IChatExecuteActionContext; + this._register(this.secondaryToolbar.onDidChangeMenuItems(() => { + // Update container reference for the pickers + const toolbarElement = this.secondaryToolbar.getElement(); + // eslint-disable-next-line no-restricted-syntax + const container = toolbarElement.querySelector('.chat-sessionPicker-container'); + this.chatSessionPickerContainer = container as HTMLElement | undefined; + })); + let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { inputModel = this.modelService.createModel('', null, this.inputUri, true); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index de3981528fcb2..3b552379ad1e8 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -51,7 +51,6 @@ const builtinDefaultIcon = (mode: IChatMode) => { case 'ask': return Codicon.ask; case 'edit': return Codicon.edit; case 'plan': return Codicon.tasklist; - case 'autopilot': return Codicon.rocket; default: return undefined; } }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts new file mode 100644 index 0000000000000..cbb1d2280b33b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { localize } from '../../../../../../nls.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { ChatPermissionLevel } from '../../../common/constants.js'; +import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; + +export interface IPermissionPickerDelegate { + readonly currentPermissionLevel: IObservable; + readonly setPermissionLevel: (level: ChatPermissionLevel) => void; +} + +export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { + constructor( + action: MenuItemAction, + private readonly delegate: IPermissionPickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const currentLevel = delegate.currentPermissionLevel.get(); + return [ + { + ...action, + id: 'chat.permissions.default', + label: localize('permissions.default', "Default Permissions"), + icon: ThemeIcon.fromId(Codicon.shield.id), + checked: currentLevel === ChatPermissionLevel.Default, + tooltip: '', + hover: { + content: localize('permissions.default.description', "Use configured auto-approve settings"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.Default); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + { + ...action, + id: 'chat.permissions.autopilot', + label: localize('permissions.autoApproveAll', "Auto-Approve All"), + icon: ThemeIcon.fromId(Codicon.warning.id), + checked: currentLevel === ChatPermissionLevel.Autopilot, + tooltip: '', + hover: { + content: localize('permissions.autoApproveAll.description', "Auto-approve all tool calls"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.Autopilot); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + ]; + } + }; + + super(action, { + actionProvider, + reporter: { id: 'ChatPermissionPicker', name: 'ChatPermissionPicker', includeOptions: true }, + }, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + + const level = this.delegate.currentPermissionLevel.get(); + const isFullAccess = level === ChatPermissionLevel.Autopilot; + const icon = isFullAccess ? Codicon.warning : Codicon.shield; + + const labelElements = []; + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + const label = isFullAccess + ? localize('permissions.autoApproveAll.label', "Auto-Approve All") + : localize('permissions.default.label', "Default Permissions"); + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + element.classList.toggle('warning', isFullAccess); + return null; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 35e910c2c37a7..711fa92ed56cb 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1309,6 +1309,77 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-top: 4px; } +/* Secondary toolbar below the input box */ +.interactive-session .chat-secondary-toolbar { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 6px 0; +} + +.interactive-session .chat-secondary-toolbar:empty { + display: none; +} + +.interactive-session .chat-secondary-toolbar > .chat-secondary-input-toolbar { + overflow: hidden; + min-width: 0px; + color: var(--vscode-icon-foreground); + + .monaco-action-bar .action-item .codicon { + color: var(--vscode-icon-foreground); + } + + .chat-input-picker-item { + min-width: 0px; + overflow: hidden; + + .action-label { + min-width: 0px; + overflow: hidden; + position: relative; + + .chat-input-picker-label { + overflow: hidden; + text-overflow: ellipsis; + } + + span + .chat-input-picker-label { + margin-left: 2px; + } + + .codicon { + font-size: 12px; + } + } + + .codicon { + flex-shrink: 0; + } + } +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label { + height: 16px; + padding: 3px 0px 3px 6px; + display: flex; + align-items: center; + color: var(--vscode-icon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.warning { + color: var(--vscode-problemsWarningIcon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.warning .codicon { + color: var(--vscode-problemsWarningIcon-foreground) !important; +} + +.monaco-workbench .interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label .codicon-chevron-down { + font-size: 12px; + margin-left: 2px; +} + .interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child { margin-right: auto; } diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts index e7d920a42d08d..65c3b1b975269 100644 --- a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -9,7 +9,7 @@ import { IsWebContext } from '../../../../../platform/contextkey/common/contextk import { RemoteNameContext } from '../../../../common/contextkeys.js'; import { ViewContainerLocation } from '../../../../common/views.js'; import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; -import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; export namespace ChatContextKeys { export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); @@ -45,6 +45,7 @@ export namespace ChatContextKeys { export const inChatTip = new RawContextKey('inChatTip', false, { type: 'boolean', description: localize('inChatTip', "True when focus is in a chat tip.") }); export const inChatTerminalToolOutput = new RawContextKey('inChatTerminalToolOutput', false, { type: 'boolean', description: localize('inChatTerminalToolOutput', "True when focus is in the chat terminal output region.") }); export const chatModeKind = new RawContextKey('chatAgentKind', ChatModeKind.Ask, { type: 'string', description: localize('agentKind', "The 'kind' of the current agent.") }); + export const chatPermissionLevel = new RawContextKey('chatPermissionLevel', ChatPermissionLevel.Default, { type: 'string', description: localize('chatPermissionLevel', "The current permission level for tool auto-approval.") }); export const chatModeName = new RawContextKey('chatModeName', '', { type: 'string', description: localize('chatModeName', "The name of the current chat mode (e.g. 'Plan' for custom modes).") }); export const chatModelId = new RawContextKey('chatModelId', '', { type: 'string', description: localize('chatModelId', "The short id of the currently selected chat model (for example 'gpt-4.1').") }); @@ -146,7 +147,6 @@ export namespace ChatContextKeyExprs { export const inEditingMode = ContextKeyExpr.or( ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Edit), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Autopilot), ); /** diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index 7b1805079a7e0..cd2cee96d7ff0 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -222,7 +222,6 @@ export class ChatModeService extends Disposable implements IChatModeService { // But hide it if the user manually disabled it via settings if (this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy()) { builtinModes.unshift(ChatMode.Agent); - builtinModes.splice(1, 0, ChatMode.Autopilot); } builtinModes.push(ChatMode.Edit); return builtinModes; @@ -531,14 +530,12 @@ export namespace ChatMode { export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question); export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit); export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent); - export const Autopilot = new BuiltinChatMode(ChatModeKind.Autopilot, 'Autopilot', localize('autopilotDescription', "Auto-approve all tools and run autonomously"), Codicon.rocket); } export function isBuiltinChatMode(mode: IChatMode): boolean { return mode.id === ChatMode.Ask.id || mode.id === ChatMode.Edit.id || - mode.id === ChatMode.Agent.id || - mode.id === ChatMode.Autopilot.id; + mode.id === ChatMode.Agent.id; } /** diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index f530d27b5ac18..ce97e57bed8a1 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DeferredPromise } from '../../../../../base/common/async.js'; +import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; @@ -40,7 +40,7 @@ import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, ICha import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../requestParser/chatRequestParser.js'; -import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; +import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatResponseErrorDetails, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSessionsService } from '../chatSessionsService.js'; import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js'; @@ -48,7 +48,7 @@ import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; import { IChatTransferService } from '../model/chatTransferService.js'; import { LocalChatSessionUri } from '../model/chatUri.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../languageModels.js'; import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; @@ -1061,7 +1061,6 @@ export class ChatService extends Disposable implements IChatService { enableCommandDetection && (location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) && options?.modeInfo?.kind !== ChatModeKind.Agent && - options?.modeInfo?.kind !== ChatModeKind.Autopilot && options?.modeInfo?.kind !== ChatModeKind.Edit && !options?.agentIdSilent ) { @@ -1182,6 +1181,12 @@ export class ChatService extends Disposable implements IChatService { shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested; request.response?.complete(); + + // Auto-retry on error when in auto-approve-all permission level + if (rawResult.errorDetails && !token.isCancellationRequested && this._shouldAutoRetry(options, attempt, rawResult.errorDetails)) { + this._scheduleAutoRetry(request, options, attempt); + } + if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request!, followups); @@ -1205,6 +1210,11 @@ export class ChatService extends Disposable implements IChatService { model.setResponse(request, rawResult); completeResponseCreated(); request.response?.complete(); + + // Auto-retry on error when in auto-approve-all permission level + if (!token.isCancellationRequested && this._shouldAutoRetry(options, attempt, rawResult.errorDetails)) { + this._scheduleAutoRetry(request, options, attempt); + } } } finally { store.dispose(); @@ -1243,6 +1253,30 @@ export class ChatService extends Disposable implements IChatService { } } + private static readonly MAX_AUTO_RETRIES = 5; + + private _shouldAutoRetry(options: IChatSendRequestOptions | undefined, attempt: number, errorDetails?: IChatResponseErrorDetails): boolean { + if (options?.modeInfo?.permissionLevel !== ChatPermissionLevel.Autopilot) { + return false; + } + if (attempt >= ChatService.MAX_AUTO_RETRIES) { + return false; + } + // Don't retry rate-limited or quota-exceeded errors + if (errorDetails?.isRateLimited || errorDetails?.isQuotaExceeded) { + return false; + } + return true; + } + + private _scheduleAutoRetry(request: IChatRequestModel, options: IChatSendRequestOptions | undefined, attempt: number): void { + const nextAttempt = attempt + 1; + this.logService.info(`[ChatService] Auto-retrying request (attempt ${nextAttempt}/${ChatService.MAX_AUTO_RETRIES}) due to auto-approve-all permission level`); + timeout(1000).then(() => { + this.resendRequest(request, { ...options, attempt: nextAttempt }); + }); + } + /** * Process the next pending request from the model's queue, if any. * Called after a request completes to continue processing queued requests. diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 50f81288f94a6..2c0c13cb30fcc 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -63,8 +63,7 @@ export enum ChatConfiguration { export enum ChatModeKind { Ask = 'ask', Edit = 'edit', - Agent = 'agent', - Autopilot = 'autopilot' + Agent = 'agent' } export function validateChatMode(mode: unknown): ChatModeKind | undefined { @@ -72,13 +71,22 @@ export function validateChatMode(mode: unknown): ChatModeKind | undefined { case ChatModeKind.Ask: case ChatModeKind.Edit: case ChatModeKind.Agent: - case ChatModeKind.Autopilot: return mode as ChatModeKind; default: return undefined; } } +/** + * The permission level controlling tool auto-approval behavior. + */ +export enum ChatPermissionLevel { + /** Use existing auto-approve settings */ + Default = 'default', + /** Auto-approve all tool calls */ + Autopilot = 'autopilot' +} + export function isChatMode(mode: unknown): mode is ChatModeKind { return !!validateChatMode(mode); } diff --git a/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts index 2c255d8e70322..58b49b13fa570 100644 --- a/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts @@ -70,7 +70,7 @@ export interface IModifiedEntryTelemetryInfo { readonly requestId: string; readonly result: IChatAgentResult | undefined; readonly modelId: string | undefined; - readonly modeId: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; + readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined; readonly feature: 'sideBarChat' | 'inlineChat' | undefined; } diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 6f14c3ef529e5..9314064e843c5 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -30,7 +30,7 @@ import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCo import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js'; import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatDisabledClaudeHooksPart, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; -import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js'; import { ToolDataSource, IToolData } from '../tools/languageModelToolsService.js'; import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js'; @@ -312,8 +312,9 @@ export interface IChatRequestModeInfo { kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' isBuiltin: boolean; modeInstructions: IChatRequestModeInstructions | undefined; - modeId: 'ask' | 'agent' | 'edit' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; + modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; applyCodeBlockSuggestionId: EditSuggestionId | undefined; + permissionLevel?: ChatPermissionLevel; } export interface IChatRequestModeInstructions { diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 7ea575983e23f..a5dbf7f25c50e 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -410,10 +410,8 @@ export class ChatAgentService extends Disposable implements IChatAgentService { } getDefaultAgent(location: ChatAgentLocation, mode: ChatModeKind = ChatModeKind.Ask): IChatAgent | undefined { - // Autopilot uses the same agent as Agent mode - const effectiveMode = mode === ChatModeKind.Autopilot ? ChatModeKind.Agent : mode; return this._preferExtensionAgent(this.getActivatedAgents().filter(a => { - if (effectiveMode && !a.modes.includes(effectiveMode)) { + if (mode && !a.modes.includes(mode)) { return false; } diff --git a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts index c417003b6afa0..f7745a0b6fa9f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -72,7 +72,7 @@ suite('ChatModeService', () => { test('should return builtin modes', () => { const modes = chatModeService.getModes(); - assert.strictEqual(modes.builtin.length, 4); + assert.strictEqual(modes.builtin.length, 3); assert.strictEqual(modes.custom.length, 0); // Check that Ask mode is always present @@ -83,24 +83,6 @@ suite('ChatModeService', () => { assert.strictEqual(askMode.kind, ChatModeKind.Ask); }); - test('should include Autopilot mode', () => { - const modes = chatModeService.getModes(); - - const autopilotMode = modes.builtin.find(mode => mode.id === ChatModeKind.Autopilot); - assert.ok(autopilotMode, 'Autopilot mode should be present'); - assert.strictEqual(autopilotMode.label.get(), 'Autopilot'); - assert.strictEqual(autopilotMode.kind, ChatModeKind.Autopilot); - }); - - test('Autopilot mode should be positioned after Agent mode', () => { - const modes = chatModeService.getModes(); - const agentIndex = modes.builtin.findIndex(mode => mode.id === ChatModeKind.Agent); - const autopilotIndex = modes.builtin.findIndex(mode => mode.id === ChatModeKind.Autopilot); - assert.ok(agentIndex >= 0, 'Agent mode should exist'); - assert.ok(autopilotIndex >= 0, 'Autopilot mode should exist'); - assert.strictEqual(autopilotIndex, agentIndex + 1, 'Autopilot should be right after Agent'); - }); - test('should adjust builtin modes based on tools agent availability', () => { // Agent mode should always be present regardless of tools agent availability chatAgentService.setHasToolsAgent(true); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts index 20ce8b30580d0..ffef6e96b809c 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.ts @@ -52,8 +52,6 @@ export interface IEditTelemetryBaseData { | 'edit' /** AI agent mode for autonomous task completion and multi-file edits */ | 'agent' - /** Autonomous agent mode with all tools auto-approved */ - | 'autopilot' /** Custom mode defined by extensions or user settings */ | 'custom' /** Applying a previously suggested code block */ diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts index 3984825c19d26..9e753e0ed6fbb 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryServiceImpl.ts @@ -42,7 +42,7 @@ export class AiEditTelemetryServiceImpl implements IAiEditTelemetryService { editLinesInserted: number | undefined; editLinesDeleted: number | undefined; - modeId: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; + modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; modelId: TelemetryTrustedValue; applyCodeBlockSuggestionId: string | undefined; }, { @@ -113,7 +113,7 @@ export class AiEditTelemetryServiceImpl implements IAiEditTelemetryService { editLinesInserted: number | undefined; editLinesDeleted: number | undefined; - modeId: 'ask' | 'edit' | 'agent' | 'autopilot' | 'custom' | 'applyCodeBlock' | undefined; + modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; modelId: TelemetryTrustedValue; applyCodeBlockSuggestionId: string | undefined; diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index f2aa5083b517b..71fd9bcd6c820 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -89,10 +89,7 @@ export class ListMcpServerCommand extends Action2 { ContextKeyExpr.and(ContextKeyExpr.equals(`config.${mcpAutoStartConfig}`, McpAutoStartValue.Never), McpContextKeys.hasUnknownTools), McpContextKeys.hasServersWithErrors, ), - ContextKeyExpr.or( - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), - ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Autopilot), - ), + ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.lockedToCodingAgent.negate(), ChatContextKeys.Setup.hidden.negate(), ), From 0cc7d20ddd35a05053ab3623e9d5a6c01815536e Mon Sep 17 00:00:00 2001 From: justschen Date: Thu, 26 Feb 2026 12:53:45 -0800 Subject: [PATCH 10/25] don't use query selector, surface toolbar in the template --- .../browser/widget/input/chatInputPart.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 302fb97e01bcc..c723f2b57635b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -364,7 +364,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private sessionTargetWidget: SessionTypePickerActionItem | undefined; private delegationWidget: DelegationSessionPickerActionItem | undefined; private chatSessionPickerWidgets: Map = new Map(); - private chatSessionPickerContainer: HTMLElement | undefined; + private chatSessionPickerContainer!: HTMLElement; private _lastSessionPickerAction: MenuItemAction | undefined; private readonly _waitForPersistedLanguageModel: MutableDisposable = this._register(new MutableDisposable()); private readonly _chatSessionOptionEmitters: Map> = new Map(); @@ -1721,9 +1721,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - if (this.chatSessionPickerContainer) { - this.chatSessionPickerContainer.style.display = ''; - } + this.chatSessionPickerContainer.style.display = ''; // Fire option change events for existing widgets to sync their state // (only if we have a session context - in welcome view, options aren't persisted yet) @@ -1750,9 +1748,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private hideAllSessionPickerWidgets(): void { - if (this.chatSessionPickerContainer) { - this.chatSessionPickerContainer.style.display = 'none'; - } + this.chatSessionPickerContainer.style.display = 'none'; } private disposeSessionPickerWidgets(): void { @@ -1959,6 +1955,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ]), ]), dom.h('.chat-secondary-toolbar@secondaryToolbar'), + dom.h('.chat-sessionPicker-container@chatSessionPickerContainer'), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attached-context@attachedContextContainer'), ]), @@ -1985,6 +1982,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ]), ]), dom.h('.chat-secondary-toolbar@secondaryToolbar'), + dom.h('.chat-sessionPicker-container@chatSessionPickerContainer'), ]); } this.container = elements.root; @@ -2006,6 +2004,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; this.secondaryToolbarContainer = elements.secondaryToolbar; + this.chatSessionPickerContainer = elements.chatSessionPickerContainer; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatGettingStartedTipContainer = elements.chatGettingStartedTipContainer; @@ -2160,7 +2159,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge actionContext: { widget }, onlyShowIconsForDefaultActions: observableFromEvent( this._inputEditor.onDidLayoutChange, - (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 450 /* Threshold for showing icon-only mode in primary toolbar pickers */ + (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 400 /* Threshold for showing icon-only mode in primary toolbar pickers */ ).recomputeInitiallyAndOnChange(this._store), hoverPosition: { forcePosition: true, @@ -2314,13 +2313,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); this.secondaryToolbar.getElement().classList.add('chat-secondary-input-toolbar'); this.secondaryToolbar.context = { widget } satisfies IChatExecuteActionContext; - this._register(this.secondaryToolbar.onDidChangeMenuItems(() => { - // Update container reference for the pickers - const toolbarElement = this.secondaryToolbar.getElement(); - // eslint-disable-next-line no-restricted-syntax - const container = toolbarElement.querySelector('.chat-sessionPicker-container'); - this.chatSessionPickerContainer = container as HTMLElement | undefined; - })); let inputModel = this.modelService.getModel(this.inputUri); if (!inputModel) { From a2847a15a6388faa811bb7779c98b9315b72c68b Mon Sep 17 00:00:00 2001 From: David Dossett <25163139+daviddossett@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:50:03 -0800 Subject: [PATCH 11/25] UI polish: context usage widget, secondary toolbar layout, theme tweaks - Move context usage widget to secondary toolbar with percentage label on hover - Adjust secondary toolbar padding/gap and input part bottom padding - Lower icon-only threshold to 300px - Darken input placeholder foreground in 2026 dark theme - Update agent mode description --- extensions/theme-2026/themes/2026-dark.json | 2 +- .../browser/widget/input/chatInputPart.ts | 18 +++++++-------- .../chat/browser/widget/media/chat.css | 17 ++++++++++---- .../viewPane/chatContextUsageWidget.ts | 9 ++++++++ .../viewPane/media/chatContextUsageWidget.css | 23 +++++++++++++++---- .../contrib/chat/common/chatModes.ts | 2 +- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json index 8d3f082600d84..615a986bd66a3 100644 --- a/extensions/theme-2026/themes/2026-dark.json +++ b/extensions/theme-2026/themes/2026-dark.json @@ -33,7 +33,7 @@ "input.background": "#191A1B", "input.border": "#333536FF", "input.foreground": "#bfbfbf", - "input.placeholderForeground": "#777777", + "input.placeholderForeground": "#555555", "inputOption.activeBackground": "#3994BC33", "inputOption.activeForeground": "#bfbfbf", "inputOption.activeBorder": "#2A2B2CFF", diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index c723f2b57635b..232868172e99a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1949,12 +1949,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ dom.h('.chat-editor-container@editorContainer'), - dom.h('.chat-input-toolbars@inputToolbars', [ - dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), - ]), + dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), - dom.h('.chat-secondary-toolbar@secondaryToolbar'), + dom.h('.chat-secondary-toolbar@secondaryToolbar', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + ]), dom.h('.chat-sessionPicker-container@chatSessionPickerContainer'), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attached-context@attachedContextContainer'), @@ -1976,12 +1976,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-attached-context@attachedContextContainer'), ]), dom.h('.chat-editor-container@editorContainer'), - dom.h('.chat-input-toolbars@inputToolbars', [ - dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), - ]), + dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), - dom.h('.chat-secondary-toolbar@secondaryToolbar'), + dom.h('.chat-secondary-toolbar@secondaryToolbar', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + ]), dom.h('.chat-sessionPicker-container@chatSessionPickerContainer'), ]); } @@ -2159,7 +2159,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge actionContext: { widget }, onlyShowIconsForDefaultActions: observableFromEvent( this._inputEditor.onDidLayoutChange, - (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 400 /* Threshold for showing icon-only mode in primary toolbar pickers */ + (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 300 /* Threshold for showing icon-only mode in primary toolbar pickers */ ).recomputeInitiallyAndOnChange(this._store), hoverPosition: { forcePosition: true, diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index 6386a6dbf6140..bdfc1171df696 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -812,11 +812,18 @@ have to be updated for changes to the rules above, or to support more deeply nes position: relative; } -/* Context usage widget container - positioned in the bottom toolbar */ -.interactive-session .chat-input-toolbars .chat-context-usage-container { +/* Context usage widget container - positioned in the secondary toolbar below input */ +.interactive-session .chat-input-toolbars .chat-context-usage-container, +.interactive-session .chat-secondary-toolbar .chat-context-usage-container { display: flex; align-items: center; flex-shrink: 0; + margin-left: auto; + order: 1; +} + +/* When context usage is inside the toolbars (compact mode), keep the ordering */ +.interactive-session .chat-input-toolbars .chat-context-usage-container { order: 1; } @@ -1317,8 +1324,8 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .chat-secondary-toolbar { display: flex; align-items: center; - gap: 4px; - padding: 4px 6px 0; + gap: 6px; + padding: 2px 5px 2px 6px; } .interactive-session .chat-secondary-toolbar:empty { @@ -1677,7 +1684,7 @@ have to be updated for changes to the rules above, or to support more deeply nes .interactive-session .interactive-input-part { margin: 0px 12px; - padding: 4px 0 12px 0px; + padding: 4px 0 6px 0px; display: flex; flex-direction: column; gap: 4px; diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts index b20f39c7cf678..e051fbcfb3f8e 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -92,6 +92,7 @@ export class ChatContextUsageWidget extends Disposable { readonly domNode: HTMLElement; private readonly progressIndicator: CircularProgressIndicator; + private readonly percentageLabel: HTMLElement; private readonly _isVisible = observableValue(this, false); get isVisible(): IObservable { return this._isVisible; } @@ -130,6 +131,9 @@ export class ChatContextUsageWidget extends Disposable { this.progressIndicator = new CircularProgressIndicator(); iconContainer.appendChild(this.progressIndicator.domNode); + // Percentage label (visible on hover/focus) + this.percentageLabel = this.domNode.appendChild($('.percentage-label')); + // Track context usage opened state this._contextUsageOpenedKey = ChatContextKeys.contextUsageHasBeenOpened.bindTo(this.contextKeyService); @@ -286,6 +290,11 @@ export class ChatContextUsageWidget extends Disposable { // Update pie chart progress this.progressIndicator.setProgress(percentage); + // Update percentage label and aria-label + const roundedPercentage = Math.round(percentage); + this.percentageLabel.textContent = `${roundedPercentage}%`; + this.domNode.setAttribute('aria-label', localize('contextUsagePercentageLabel', "Context window usage: {0}%", roundedPercentage)); + // Update color based on usage level this.domNode.classList.remove('warning', 'error'); if (percentage >= 90) { diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css index c3abb1332f0de..e09c5e1aa2321 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css @@ -6,9 +6,7 @@ .chat-context-usage-widget { display: flex; align-items: center; - justify-content: center; - height: 22px; - width: 22px; + gap: 4px; flex-shrink: 0; cursor: pointer; padding: 3px; @@ -55,7 +53,7 @@ .chat-context-usage-widget .progress-arc { fill: none; - stroke: var(--vscode-descriptionForeground); + stroke: var(--vscode-icon-foreground); stroke-width: 4; stroke-linecap: round; transform: rotate(-90deg); @@ -70,3 +68,20 @@ .chat-context-usage-widget.error .progress-arc { stroke: var(--vscode-editorError-foreground); } + +.chat-context-usage-widget .percentage-label { + font-size: 11px; + line-height: 1; + color: var(--vscode-descriptionForeground); + white-space: nowrap; + max-width: 0; + opacity: 0; + overflow: hidden; + transition: max-width 0.1s ease-out, opacity 0.1s ease-out; +} + +.chat-context-usage-widget:hover .percentage-label, +.chat-context-usage-widget:focus .percentage-label { + max-width: 4em; + opacity: 1; +} diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index e2f94e4269c9f..f55117109721d 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -539,7 +539,7 @@ export class BuiltinChatMode implements IChatMode { export namespace ChatMode { export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question); export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit); - export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent); + export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build"), Codicon.agent); } export function isBuiltinChatMode(mode: IChatMode): boolean { From bd778df1f091356089a48f0365ebf9935a0492fb Mon Sep 17 00:00:00 2001 From: justschen Date: Thu, 26 Feb 2026 14:08:52 -0800 Subject: [PATCH 12/25] update names --- .../widget/input/permissionPickerActionItem.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index cbb1d2280b33b..8b01fc0a401ff 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -41,12 +41,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { { ...action, id: 'chat.permissions.default', - label: localize('permissions.default', "Default Permissions"), + label: localize('permissions.default', "Default Approvals"), icon: ThemeIcon.fromId(Codicon.shield.id), checked: currentLevel === ChatPermissionLevel.Default, tooltip: '', hover: { - content: localize('permissions.default.description', "Use configured auto-approve settings"), + content: localize('permissions.default.description', "Use configured approval settings"), position: pickerOptions.hoverPosition }, run: async () => { @@ -59,12 +59,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { { ...action, id: 'chat.permissions.autopilot', - label: localize('permissions.autoApproveAll', "Auto-Approve All"), + label: localize('permissions.autoApproveAll', "Auto Approvals"), icon: ThemeIcon.fromId(Codicon.warning.id), checked: currentLevel === ChatPermissionLevel.Autopilot, tooltip: '', hover: { - content: localize('permissions.autoApproveAll.description', "Auto-approve all tool calls"), + content: localize('permissions.autoApproveAll.description', "Automatically approve all tool calls"), position: pickerOptions.hoverPosition }, run: async () => { @@ -94,8 +94,8 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { const labelElements = []; labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); const label = isFullAccess - ? localize('permissions.autoApproveAll.label', "Auto-Approve All") - : localize('permissions.default.label', "Default Permissions"); + ? localize('permissions.autoApproveAll.label', "Auto Approvals") + : localize('permissions.default.label', "Default Approvals"); labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); From a014c3ea2167c6c788b4237c8f87e32d6062dc2f Mon Sep 17 00:00:00 2001 From: justschen Date: Thu, 26 Feb 2026 15:50:13 -0800 Subject: [PATCH 13/25] update api for tool call limits --- src/vs/workbench/api/common/extHostTypeConverters.ts | 1 + .../contrib/chat/common/chatService/chatServiceImpl.ts | 1 + .../contrib/chat/common/participants/chatAgents.ts | 5 +++++ src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts | 6 ++++++ 4 files changed, 13 insertions(+) diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index d565aa7634c67..5fe610565a683 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -3440,6 +3440,7 @@ export namespace ChatAgentRequest { editedFileEvents: request.editedFileEvents, modeInstructions: request.modeInstructions?.content, modeInstructions2: ChatRequestModeInstructions.to(request.modeInstructions), + permissionLevel: request.permissionLevel, subAgentInvocationId: request.subAgentInvocationId, subAgentName: request.subAgentName, parentRequestId: request.parentRequestId, diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index c853a8b2cb122..a420ad8388652 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -1028,6 +1028,7 @@ export class ChatService extends Disposable implements IChatService { userSelectedModelId: options?.userSelectedModelId, userSelectedTools: options?.userSelectedTools?.get(), modeInstructions: options?.modeInfo?.modeInstructions, + permissionLevel: options?.modeInfo?.permissionLevel, editedFileEvents: request.editedFileEvents, hooks: collectedHooks, hasHooksEnabled: !!collectedHooks && Object.values(collectedHooks).some(arr => arr.length > 0), diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index a5dbf7f25c50e..b851a75269f13 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -157,6 +157,11 @@ export interface IChatAgentRequest { * Whether any hooks are enabled for this request. */ hasHooksEnabled?: boolean; + /** + * The permission level for tool auto-approval in this request. + * When set to 'autopilot', all tool calls and confirmations should be auto-approved. + */ + permissionLevel?: string; /** * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 16c8ff488a027..8dca8cb92979f 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -116,6 +116,12 @@ declare module 'vscode' { */ readonly parentRequestId?: string; + /** + * The permission level for tool auto-approval in this request. + * When set to 'autopilot', all tool calls and confirmations should be auto-approved. + */ + readonly permissionLevel?: string; + /** * Whether any hooks are enabled for this request. */ From f1b6205b510a0ee8e81f7fba7268bd1dd0075dce Mon Sep 17 00:00:00 2001 From: justschen Date: Thu, 26 Feb 2026 17:25:54 -0800 Subject: [PATCH 14/25] add true autopilot --- .../tools/languageModelToolsService.ts | 6 +- .../chat/browser/widget/chatListRenderer.ts | 6 +- .../contrib/chat/browser/widget/chatWidget.ts | 7 ++- .../browser/widget/input/chatInputPart.ts | 17 ++++++ .../input/permissionPickerActionItem.ts | 54 +++++++++++++++--- .../chat/browser/widget/media/chat.css | 8 +++ .../common/chatService/chatServiceImpl.ts | 4 +- .../contrib/chat/common/constants.ts | 12 +++- .../chat/common/participants/chatAgents.ts | 3 +- .../tools/builtinTools/taskCompleteTool.ts | 56 +++++++++++++++++++ .../chat/common/tools/builtinTools/tools.ts | 5 +- ...scode.proposed.chatParticipantPrivate.d.ts | 3 +- 12 files changed, 159 insertions(+), 22 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index e030eb25f3224..1a6a798b5c58e 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -40,7 +40,7 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { ChatRequestToolReferenceEntry, toToolSetVariableEntry, toToolVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { IVariableReference } from '../../common/chatModes.js'; import { ConfirmedReason, IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js'; -import { ChatConfiguration, ChatPermissionLevel } from '../../common/constants.js'; +import { ChatConfiguration, isAutoApproveLevel } from '../../common/constants.js'; import { ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IChatModel, IChatRequestModel } from '../../common/model/chatModel.js'; import { ChatToolInvocation } from '../../common/model/chatProgressTypes/chatToolInvocation.js'; @@ -1047,7 +1047,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); - if (request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } @@ -1088,7 +1088,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); - if (request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts index 1cf8b39f364e6..d88187341bea9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -64,7 +64,7 @@ import { IChatRequestVariableEntry } from '../../common/attachments/chatVariable import { IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM, IChatPendingDividerViewModel, isPendingDividerVM } from '../../common/model/chatViewModel.js'; import { getNWords } from '../../common/model/chatWordCounter.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; import { ClickAnimation } from '../../../../../base/browser/ui/animations/animations.js'; import { MarkHelpfulActionId, MarkUnhelpfulActionId } from '../actions/chatTitleActions.js'; import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from '../chat.js'; @@ -2313,7 +2313,9 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - if (!shouldAutoReply) { + // always autoreply in autopilot mode. + const isAutopilot = isResponseVM(context.element) && context.element.model.request?.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot; + if (!shouldAutoReply && !isAutopilot) { // Roll back the in-progress mark if auto-reply is not enabled. if (stableKey) { this._autoRepliedQuestionCarousels.delete(stableKey); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 48e7be78cd212..1a7d69361603e 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -66,7 +66,7 @@ import { IChatTodoListService } from '../../common/tools/chatTodoListService.js' import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../../common/constants.js'; import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../common/promptSyntax/config/config.js'; @@ -1722,6 +1722,7 @@ export class ChatWidget extends Disposable implements IChatWidget { rowContainer.appendChild(this.inputContainer); this.createInput(this.inputContainer); this.input.setChatMode(this.inputPart.currentModeObs.get().id); + this.input.setPermissionLevel(this.inputPart.currentModeInfo.permissionLevel ?? ChatPermissionLevel.Default); this.input.setEditing(true, isEditingSentRequest); this._onDidChangeActiveInputEditor.fire(); } else { @@ -1822,6 +1823,7 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!isInput) { this.inputPart.setChatMode(this.input.currentModeObs.get().id); + this.inputPart.setPermissionLevel(this.input.currentModeInfo.permissionLevel ?? ChatPermissionLevel.Default); const currentModel = this.input.selectedLanguageModel.get(); if (currentModel) { this.inputPart.switchModel(currentModel.metadata); @@ -2299,7 +2301,8 @@ export class ChatWidget extends Disposable implements IChatWidget { const options: IChatSendRequestOptions = { attempt: lastRequest.attempt + 1, location: this.location, - userSelectedModelId: this.input.currentLanguageModel + userSelectedModelId: this.input.currentLanguageModel, + modeInfo: this.input.currentModeInfo, }; return await this.chatService.resendRequest(lastRequest, options); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 232868172e99a..26563ab715d0b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -409,6 +409,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this._currentModeObservable; } + public get currentPermissionLevelObs(): IObservable { + return this._currentPermissionLevel; + } + public get currentModeInfo(): IChatRequestModeInfo { const mode = this._currentModeObservable.get(); const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom'; @@ -801,6 +805,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.permissionWidget?.show(); } + public setPermissionLevel(level: ChatPermissionLevel): void { + this._currentPermissionLevel.set(level, undefined); + this.permissionLevelKey.set(level); + this.permissionWidget?.refresh(); + } + public openSessionTargetPicker(): void { this.sessionTargetWidget?.show(); } @@ -887,6 +897,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.selectedToolsModel.resetSessionEnablementState(); this._chatSessionIsEmpty = chatSessionIsEmpty; + // Reset permission level to default on new sessions + if (chatSessionIsEmpty) { + this._currentPermissionLevel.set(ChatPermissionLevel.Default, undefined); + this.permissionLevelKey.set(ChatPermissionLevel.Default); + this.permissionWidget?.refresh(); + } + // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. if (chatSessionIsEmpty) { this._setEmptyModelState(); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 8b01fc0a401ff..645a75c6a6b39 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -58,13 +58,31 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { } satisfies IActionWidgetDropdownAction, { ...action, - id: 'chat.permissions.autopilot', - label: localize('permissions.autoApproveAll', "Auto Approvals"), + id: 'chat.permissions.autoApprove', + label: localize('permissions.autoApprove', "Auto Approvals"), icon: ThemeIcon.fromId(Codicon.warning.id), + checked: currentLevel === ChatPermissionLevel.AutoApprove, + tooltip: '', + hover: { + content: localize('permissions.autoApprove.description', "Auto-approve all tool calls, retry on errors, skip on max requests"), + position: pickerOptions.hoverPosition + }, + run: async () => { + delegate.setPermissionLevel(ChatPermissionLevel.AutoApprove); + if (this.element) { + this.renderLabel(this.element); + } + }, + } satisfies IActionWidgetDropdownAction, + { + ...action, + id: 'chat.permissions.autopilot', + label: localize('permissions.autopilot', "Autopilot"), + icon: ThemeIcon.fromId(Codicon.rocket.id), checked: currentLevel === ChatPermissionLevel.Autopilot, tooltip: '', hover: { - content: localize('permissions.autoApproveAll.description', "Automatically approve all tool calls"), + content: localize('permissions.autopilot.description', "Auto-approve all tool calls and continue until the task is done"), position: pickerOptions.hoverPosition }, run: async () => { @@ -88,19 +106,37 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { this.setAriaLabelAttributes(element); const level = this.delegate.currentPermissionLevel.get(); - const isFullAccess = level === ChatPermissionLevel.Autopilot; - const icon = isFullAccess ? Codicon.warning : Codicon.shield; + let icon: ThemeIcon; + let label: string; + switch (level) { + case ChatPermissionLevel.Autopilot: + icon = Codicon.rocket; + label = localize('permissions.autopilot.label', "Autopilot"); + break; + case ChatPermissionLevel.AutoApprove: + icon = Codicon.warning; + label = localize('permissions.autoApprove.label', "Auto Approvals"); + break; + default: + icon = Codicon.shield; + label = localize('permissions.default.label', "Default Approvals"); + break; + } const labelElements = []; labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); - const label = isFullAccess - ? localize('permissions.autoApproveAll.label', "Auto Approvals") - : localize('permissions.default.label', "Default Approvals"); labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); dom.reset(element, ...labelElements); - element.classList.toggle('warning', isFullAccess); + element.classList.toggle('warning', level === ChatPermissionLevel.Autopilot); + element.classList.toggle('info', level === ChatPermissionLevel.AutoApprove); return null; } + + public refresh(): void { + if (this.element) { + this.renderLabel(this.element); + } + } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index ec04947a3cddd..7e8248c21a382 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1386,6 +1386,14 @@ have to be updated for changes to the rules above, or to support more deeply nes color: var(--vscode-problemsWarningIcon-foreground) !important; } +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.info { + color: var(--vscode-problemsInfoIcon-foreground); +} + +.interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label.info .codicon { + color: var(--vscode-problemsInfoIcon-foreground) !important; +} + .monaco-workbench .interactive-session .chat-secondary-toolbar .chat-input-picker-item .action-label .codicon-chevron-down { font-size: 12px; margin-left: 2px; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index a420ad8388652..ab3c44d0b18c2 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -48,7 +48,7 @@ import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; import { IChatTransferService } from '../model/chatTransferService.js'; import { LocalChatSessionUri } from '../model/chatUri.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, isAutoApproveLevel } from '../constants.js'; import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../languageModels.js'; import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; @@ -1257,7 +1257,7 @@ export class ChatService extends Disposable implements IChatService { private static readonly MAX_AUTO_RETRIES = 5; private _shouldAutoRetry(options: IChatSendRequestOptions | undefined, attempt: number, errorDetails?: IChatResponseErrorDetails): boolean { - if (options?.modeInfo?.permissionLevel !== ChatPermissionLevel.Autopilot) { + if (!isAutoApproveLevel(options?.modeInfo?.permissionLevel)) { return false; } if (attempt >= ChatService.MAX_AUTO_RETRIES) { diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 0986e4dc551d1..66974e5f889d6 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -85,10 +85,20 @@ export function validateChatMode(mode: unknown): ChatModeKind | undefined { export enum ChatPermissionLevel { /** Use existing auto-approve settings */ Default = 'default', - /** Auto-approve all tool calls */ + /** Auto-approve all tool calls, auto-retry on error, silently increase tool call limits */ + AutoApprove = 'autoApprove', + /** Everything AutoApprove does plus an internal stop hook that continues until the task is done */ Autopilot = 'autopilot' } +/** + * Returns true if the permission level enables auto-approval of all tool calls. + * Both {@link ChatPermissionLevel.AutoApprove} and {@link ChatPermissionLevel.Autopilot} enable auto-approval. + */ +export function isAutoApproveLevel(level: ChatPermissionLevel | undefined): boolean { + return level === ChatPermissionLevel.AutoApprove || level === ChatPermissionLevel.Autopilot; +} + export function isChatMode(mode: unknown): mode is ChatModeKind { return !!validateChatMode(mode); } diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index b851a75269f13..cec9e53e18ec2 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -159,7 +159,8 @@ export interface IChatAgentRequest { hasHooksEnabled?: boolean; /** * The permission level for tool auto-approval in this request. - * When set to 'autopilot', all tool calls and confirmations should be auto-approved. + * - `'autoApprove'`: Auto-approve all tool calls, retry on errors, skip on max requests. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. */ permissionLevel?: string; /** diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts new file mode 100644 index 0000000000000..3b0f5f5f25afa --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress, CountTokensCallback } from '../languageModelToolsService.js'; + +export const TaskCompleteToolId = 'task_complete'; + +/** + * Message sent to the agent when the session goes idle without task completion. + */ +export const AUTOPILOT_CONTINUATION_MESSAGE = + 'You have not yet called task_complete. If you are still planning, stop planning and start working. ' + + 'If you hit an error, try to resolve it or find another approach. ' + + 'Keep going until the task is fully done, then call task_complete.'; + +export const TaskCompleteToolData: IToolData = { + id: TaskCompleteToolId, + displayName: 'Task Complete', + modelDescription: + 'Signal that the user\'s task is fully done. Call this only after you have made all changes, ' + + 'verified they work (e.g. no compile errors, tests pass if relevant), and are confident nothing remains. ' + + 'Provide a brief summary of what was accomplished. If the summary is trivial (e.g. answering a question), omit it. ' + + 'Do not restate the summary in your message text — it is shown to the user directly.', + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + summary: { + type: 'string', + description: 'Brief summary of what was accomplished. Omit for trivial interactions.', + }, + }, + }, +}; + +export class TaskCompleteTool implements IToolImpl { + async prepareToolInvocation(_context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + return { + presentation: ToolInvocationPresentation.Hidden, + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, _token: CancellationToken): Promise { + const params = invocation.parameters as { summary?: string }; + const summary = params?.summary ?? 'All done!'; + return { + content: [{ + kind: 'text', + value: summary, + }], + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 0258444f00b34..4fdd751f6a286 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -12,6 +12,7 @@ import { ConfirmationTool, ConfirmationToolData, ConfirmationToolWithOptionsData import { EditTool, EditToolData } from './editFileTool.js'; import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; import { RunSubagentTool } from './runSubagentTool.js'; +import { TaskCompleteTool, TaskCompleteToolData } from './taskCompleteTool.js'; export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { @@ -34,11 +35,13 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool)); this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); - // Register the confirmation tool const confirmationTool = instantiationService.createInstance(ConfirmationTool); this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); this._register(toolsService.registerTool(ConfirmationToolWithOptionsData, confirmationTool)); + const taskCompleteTool = instantiationService.createInstance(TaskCompleteTool); + this._register(toolsService.registerTool(TaskCompleteToolData, taskCompleteTool)); + const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); let runSubagentRegistration: IDisposable | undefined; diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index 8dca8cb92979f..e40d6446a06f6 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -118,7 +118,8 @@ declare module 'vscode' { /** * The permission level for tool auto-approval in this request. - * When set to 'autopilot', all tool calls and confirmations should be auto-approved. + * - `'autoApprove'`: Auto-approve all tool calls, retry on errors, skip on max requests. + * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. */ readonly permissionLevel?: string; From 6e450639b30a2866b496c6bf65b5fde9fb4546a2 Mon Sep 17 00:00:00 2001 From: justschen Date: Sat, 28 Feb 2026 10:00:46 -0800 Subject: [PATCH 15/25] move error retry logic to extension --- .../tools/languageModelToolsService.ts | 16 +++++--- .../browser/widget/input/chatInputPart.ts | 8 +++- .../input/permissionPickerActionItem.ts | 2 +- .../common/chatService/chatServiceImpl.ts | 40 ++----------------- .../contrib/chat/common/constants.ts | 2 +- .../chat/common/participants/chatAgents.ts | 2 +- .../tools/builtinTools/taskCompleteTool.ts | 2 +- ...scode.proposed.chatParticipantPrivate.d.ts | 2 +- 8 files changed, 24 insertions(+), 50 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 1a6a798b5c58e..f31936d3c39ec 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -638,7 +638,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo this.ensureToolDetails(dto, toolResult, tool.data); const afterExecuteState = await toolInvocation?.didExecuteTool(toolResult, undefined, () => - this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource)); + this.shouldAutoConfirmPostExecution(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, dto.context?.sessionResource, dto.chatRequestId)); if (toolInvocation && afterExecuteState?.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { const postConfirm = await IChatToolInvocation.awaitPostConfirmation(toolInvocation, token); @@ -788,7 +788,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } // No hook decision - use normal auto-confirm logic - const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource); + const autoConfirmed = await this.shouldAutoConfirm(tool.data.id, tool.data.runsInWorkspace, tool.data.source, dto.parameters, sessionResource, dto.chatRequestId); return { autoConfirmed, preparedInvocation }; } @@ -1037,7 +1037,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return true; } - private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + private async shouldAutoConfirm(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { const tool = this._tools.get(toolId); if (!tool) { return undefined; @@ -1046,7 +1046,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Auto-Approve All permission level bypasses all tool confirmations if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); + const request = chatRequestId + ? model?.getRequests().find(r => r.id === chatRequestId) + : model?.getRequests().at(-1); if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } @@ -1083,11 +1085,13 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined): Promise { + private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { // Auto-Approve All permission level bypasses all post-execution confirmations if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); - const request = model?.getRequests().at(-1); + const request = chatRequestId + ? model?.getRequests().find(r => r.id === chatRequestId) + : model?.getRequests().at(-1); if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 8c273d252ca79..0d9f6b02e6c5c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -1741,7 +1741,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } - this.chatSessionPickerContainer.style.display = ''; + if (this.chatSessionPickerContainer) { + this.chatSessionPickerContainer.style.display = ''; + } // Fire option change events for existing widgets to sync their state // (only if we have a session context - in welcome view, options aren't persisted yet) @@ -1768,7 +1770,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private hideAllSessionPickerWidgets(): void { - this.chatSessionPickerContainer.style.display = 'none'; + if (this.chatSessionPickerContainer) { + this.chatSessionPickerContainer.style.display = 'none'; + } } private disposeSessionPickerWidgets(): void { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 645a75c6a6b39..66a86bdd90d1d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -64,7 +64,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { checked: currentLevel === ChatPermissionLevel.AutoApprove, tooltip: '', hover: { - content: localize('permissions.autoApprove.description', "Auto-approve all tool calls, retry on errors, skip on max requests"), + content: localize('permissions.autoApprove.description', "Auto-approve all tool calls and retry on errors"), position: pickerOptions.hoverPosition }, run: async () => { diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index affc6ce4940bb..def73a319f04c 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DeferredPromise, timeout } from '../../../../../base/common/async.js'; +import { DeferredPromise } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; @@ -40,7 +40,7 @@ import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, ICha import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js'; import { ChatRequestParser } from '../requestParser/chatRequestParser.js'; -import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatResponseErrorDetails, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; +import { ChatMcpServersStarting, ChatPendingRequestChangeClassification, ChatPendingRequestChangeEvent, ChatPendingRequestChangeEventName, ChatRequestQueueKind, ChatSendResult, ChatSendResultQueued, ChatStopCancellationNoopClassification, ChatStopCancellationNoopEvent, ChatStopCancellationNoopEventName, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSessionsService } from '../chatSessionsService.js'; import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js'; @@ -48,7 +48,7 @@ import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; import { IChatTransferService } from '../model/chatTransferService.js'; import { LocalChatSessionUri } from '../model/chatUri.js'; import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind, isAutoApproveLevel } from '../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; import { ChatMessageRole, IChatMessage, ILanguageModelsService } from '../languageModels.js'; import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; @@ -1183,11 +1183,6 @@ export class ChatService extends Disposable implements IChatService { shouldProcessPending = !rawResult.errorDetails && !token.isCancellationRequested; request.response?.complete(); - // Auto-retry on error when in auto-approve-all permission level - if (rawResult.errorDetails && !token.isCancellationRequested && this._shouldAutoRetry(options, attempt, rawResult.errorDetails)) { - this._scheduleAutoRetry(request, options, attempt); - } - if (agentOrCommandFollowups) { agentOrCommandFollowups.then(followups => { model.setFollowups(request!, followups); @@ -1211,11 +1206,6 @@ export class ChatService extends Disposable implements IChatService { model.setResponse(request, rawResult); completeResponseCreated(); request.response?.complete(); - - // Auto-retry on error when in auto-approve-all permission level - if (!token.isCancellationRequested && this._shouldAutoRetry(options, attempt, rawResult.errorDetails)) { - this._scheduleAutoRetry(request, options, attempt); - } } } finally { store.dispose(); @@ -1254,30 +1244,6 @@ export class ChatService extends Disposable implements IChatService { } } - private static readonly MAX_AUTO_RETRIES = 5; - - private _shouldAutoRetry(options: IChatSendRequestOptions | undefined, attempt: number, errorDetails?: IChatResponseErrorDetails): boolean { - if (!isAutoApproveLevel(options?.modeInfo?.permissionLevel)) { - return false; - } - if (attempt >= ChatService.MAX_AUTO_RETRIES) { - return false; - } - // Don't retry rate-limited or quota-exceeded errors - if (errorDetails?.isRateLimited || errorDetails?.isQuotaExceeded) { - return false; - } - return true; - } - - private _scheduleAutoRetry(request: IChatRequestModel, options: IChatSendRequestOptions | undefined, attempt: number): void { - const nextAttempt = attempt + 1; - this.logService.info(`[ChatService] Auto-retrying request (attempt ${nextAttempt}/${ChatService.MAX_AUTO_RETRIES}) due to auto-approve-all permission level`); - timeout(1000).then(() => { - this.resendRequest(request, { ...options, attempt: nextAttempt }); - }); - } - /** * Process the next pending request from the model's queue, if any. * Called after a request completes to continue processing queued requests. diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index ba5c651263730..beffd2f272763 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -84,7 +84,7 @@ export function validateChatMode(mode: unknown): ChatModeKind | undefined { export enum ChatPermissionLevel { /** Use existing auto-approve settings */ Default = 'default', - /** Auto-approve all tool calls, auto-retry on error, silently increase tool call limits */ + /** Auto-approve all tool calls, auto-retry on error */ AutoApprove = 'autoApprove', /** Everything AutoApprove does plus an internal stop hook that continues until the task is done */ Autopilot = 'autopilot' diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index cec9e53e18ec2..72cc551691684 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -159,7 +159,7 @@ export interface IChatAgentRequest { hasHooksEnabled?: boolean; /** * The permission level for tool auto-approval in this request. - * - `'autoApprove'`: Auto-approve all tool calls, retry on errors, skip on max requests. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. */ permissionLevel?: string; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts index 3b0f5f5f25afa..764d6cb596174 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -39,7 +39,7 @@ export const TaskCompleteToolData: IToolData = { export class TaskCompleteTool implements IToolImpl { async prepareToolInvocation(_context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { return { - presentation: ToolInvocationPresentation.Hidden, + presentation: ToolInvocationPresentation.HiddenAfterComplete, }; } diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index e40d6446a06f6..b47de764d3cc3 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -118,7 +118,7 @@ declare module 'vscode' { /** * The permission level for tool auto-approval in this request. - * - `'autoApprove'`: Auto-approve all tool calls, retry on errors, skip on max requests. + * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. */ readonly permissionLevel?: string; From 1333e22e5481e11178260713ee8be500a76d057d Mon Sep 17 00:00:00 2001 From: justschen Date: Sun, 1 Mar 2026 13:31:42 -0800 Subject: [PATCH 16/25] address some more comments --- .../contrib/chat/browser/tools/languageModelToolsService.ts | 4 +++- .../workbench/contrib/chat/common/participants/chatAgents.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index f31936d3c39ec..2cc75b837da1e 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -445,7 +445,9 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo let request: IChatRequestModel | undefined; if (dto.context?.sessionResource) { model = this._chatService.getSession(dto.context.sessionResource); - request = model?.getRequests().at(-1); + request = (dto.chatRequestId + ? model?.getRequests().find(r => r.id === dto.chatRequestId) + : undefined) ?? model?.getRequests().at(-1); if (request?.response?.isCanceled || request?.response?.isComplete) { this._logService.debug(`[LanguageModelToolsService#invokeTool] Ignoring tool ${dto.toolId} for cancelled/complete request ${request.id}`); throw new CancellationError(); diff --git a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 72cc551691684..f422fdb89d508 100644 --- a/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -24,7 +24,7 @@ import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRe import { IChatRequestHooks } from '../promptSyntax/hookSchema.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from '../chatService/chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel } from '../constants.js'; import { ILanguageModelsService } from '../languageModels.js'; //#region agent service, commands etc @@ -162,7 +162,7 @@ export interface IChatAgentRequest { * - `'autoApprove'`: Auto-approve all tool calls and retry on errors. * - `'autopilot'`: Everything autoApprove does plus continues until the task is done. */ - permissionLevel?: string; + permissionLevel?: ChatPermissionLevel; /** * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ From c475ddaad353dc5283ff9271ab814596dce1f7c1 Mon Sep 17 00:00:00 2001 From: justschen Date: Sun, 1 Mar 2026 22:36:21 -0800 Subject: [PATCH 17/25] make sure to hide tool --- .../chat/browser/tools/languageModelToolsService.ts | 12 +++--------- src/vs/workbench/contrib/chat/common/chatModes.ts | 3 +-- .../common/tools/builtinTools/taskCompleteTool.ts | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index 2cc75b837da1e..9f572f21ed87e 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -445,9 +445,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo let request: IChatRequestModel | undefined; if (dto.context?.sessionResource) { model = this._chatService.getSession(dto.context.sessionResource); - request = (dto.chatRequestId - ? model?.getRequests().find(r => r.id === dto.chatRequestId) - : undefined) ?? model?.getRequests().at(-1); + request = model?.getRequests().at(-1); if (request?.response?.isCanceled || request?.response?.isComplete) { this._logService.debug(`[LanguageModelToolsService#invokeTool] Ignoring tool ${dto.toolId} for cancelled/complete request ${request.id}`); throw new CancellationError(); @@ -1048,9 +1046,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Auto-Approve All permission level bypasses all tool confirmations if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); - const request = chatRequestId - ? model?.getRequests().find(r => r.id === chatRequestId) - : model?.getRequests().at(-1); + const request = model?.getRequests().at(-1); if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } @@ -1091,9 +1087,7 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo // Auto-Approve All permission level bypasses all post-execution confirmations if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); - const request = chatRequestId - ? model?.getRequests().find(r => r.id === chatRequestId) - : model?.getRequests().at(-1); + const request = model?.getRequests().at(-1); if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } diff --git a/src/vs/workbench/contrib/chat/common/chatModes.ts b/src/vs/workbench/contrib/chat/common/chatModes.ts index f55117109721d..f6501831c4cd6 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -129,7 +129,6 @@ export class ChatModeService extends Disposable implements IChatModeService { target: cachedMode.target ?? Target.Undefined, visibility, agents: cachedMode.agents, - source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local } }; const instance = new CustomChatMode(customChatMode); @@ -444,7 +443,7 @@ export class CustomChatMode implements IChatMode { source: serializeChatModeSource(this._source), target: this.target.get(), visibility: this.visibility.get(), - agents: this.agents.get(), + agents: this.agents.get() }; } } diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts index 764d6cb596174..3b0f5f5f25afa 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -39,7 +39,7 @@ export const TaskCompleteToolData: IToolData = { export class TaskCompleteTool implements IToolImpl { async prepareToolInvocation(_context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { return { - presentation: ToolInvocationPresentation.HiddenAfterComplete, + presentation: ToolInvocationPresentation.Hidden, }; } From 4cf070d6c327fc9b26eca4927907f061960c87d0 Mon Sep 17 00:00:00 2001 From: justschen Date: Sun, 1 Mar 2026 23:58:28 -0800 Subject: [PATCH 18/25] bump version # --- src/vs/platform/extensions/common/extensionsApiProposals.ts | 2 +- src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index fb796867b8bc2..a9bc2d2fa1092 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -60,7 +60,7 @@ const _allApiProposals = { }, chatParticipantPrivate: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts', - version: 14 + version: 15 }, chatPromptFiles: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatPromptFiles.d.ts', diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index b47de764d3cc3..839499ef55f04 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 14 +// version: 15 declare module 'vscode' { From d0072c11c9da8087c5b70fe4a0b4d30df330fb98 Mon Sep 17 00:00:00 2001 From: justschen Date: Mon, 2 Mar 2026 14:31:14 -0800 Subject: [PATCH 19/25] better tool description --- .../input/permissionPickerActionItem.ts | 4 +-- .../tools/builtinTools/askQuestionsTool.ts | 31 +++++++++++++++++++ .../tools/builtinTools/taskCompleteTool.ts | 28 +++++++++++++---- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index 66a86bdd90d1d..d196b24b5f358 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -59,7 +59,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { { ...action, id: 'chat.permissions.autoApprove', - label: localize('permissions.autoApprove', "Auto Approvals"), + label: localize('permissions.autoApprove', "Bypass Approvals"), icon: ThemeIcon.fromId(Codicon.warning.id), checked: currentLevel === ChatPermissionLevel.AutoApprove, tooltip: '', @@ -115,7 +115,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { break; case ChatPermissionLevel.AutoApprove: icon = Codicon.warning; - label = localize('permissions.autoApprove.label', "Auto Approvals"); + label = localize('permissions.autoApprove.label', "Bypass Approvals"); break; default: icon = Codicon.shield; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index bf7dd424e25dc..57505cb3e0bea 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -13,6 +13,7 @@ import { localize } from '../../../../../../nls.js'; import { IChatQuestion, IChatService } from '../../chatService/chatService.js'; import { ChatQuestionCarouselData } from '../../model/chatProgressTypes/chatQuestionCarouselData.js'; import { IChatRequestModel } from '../../model/chatModel.js'; +import { ChatPermissionLevel } from '../../constants.js'; import { StopWatch } from '../../../../../../base/common/stopwatch.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; @@ -22,6 +23,12 @@ import { Codicon } from '../../../../../../base/common/codicons.js'; import { raceCancellation } from '../../../../../../base/common/async.js'; import { URI } from '../../../../../../base/common/uri.js'; +/** + * Response returned to the model when the user is not available (autopilot mode). + */ +export const AUTOPILOT_ASK_USER_RESPONSE = + 'The user is not available to respond and will review your work later. Work autonomously and make good decisions.'; + // Use a distinct id to avoid clashing with extension-provided tools export const AskQuestionsToolId = 'vscode_askQuestions'; @@ -187,6 +194,12 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { return this.createSkippedResult(questions); } + // In autopilot mode, the user is not available — auto-respond instead of blocking. + if (request.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { + this.logService.info('[AskQuestionsTool] Autopilot mode: auto-responding to questions'); + return this.createAutopilotResult(questions); + } + const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions); this.chatService.appendProgress(request, carousel); @@ -477,6 +490,24 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { }; } + private createAutopilotResult(questions: IQuestion[]): IToolResult { + const answers: Record = {}; + for (const question of questions) { + // Pick the recommended option if available, otherwise pick the first option + const recommended = question.options?.find(opt => opt.recommended); + const firstOption = question.options?.[0]; + const selected = recommended?.label ?? firstOption?.label; + answers[question.header] = { + selected: selected ? [selected] : [], + freeText: selected ? null : AUTOPILOT_ASK_USER_RESPONSE, + skipped: false, + }; + } + return { + content: [{ kind: 'text', value: JSON.stringify({ answers } satisfies IAnswerResult) }] + }; + } + private sendTelemetry(requestId: string | undefined, questionCount: number, answeredCount: number, skippedCount: number, freeTextCount: number, recommendedAvailableCount: number, recommendedSelectedCount: number, duration: number): void { this.telemetryService.publicLog2('askQuestionsToolInvoked', { requestId, diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts index 3b0f5f5f25afa..6ff6314e04e99 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -12,18 +12,34 @@ export const TaskCompleteToolId = 'task_complete'; * Message sent to the agent when the session goes idle without task completion. */ export const AUTOPILOT_CONTINUATION_MESSAGE = - 'You have not yet called task_complete. If you are still planning, stop planning and start working. ' + - 'If you hit an error, try to resolve it or find another approach. ' + - 'Keep going until the task is fully done, then call task_complete.'; + 'You have not yet marked the task as complete using the task_complete tool. ' + + 'If you were planning, stop planning and start implementing. ' + + 'You are not done until you have fully completed the task.\n\n' + + 'IMPORTANT: Do NOT call task_complete if:\n' + + '- You have open questions or ambiguities — make good decisions and keep working\n' + + '- You encountered an error — try to resolve it or find an alternative approach\n' + + '- There are remaining steps — complete them first\n\n' + + 'Keep working autonomously until the task is truly finished, then call task_complete.'; export const TaskCompleteToolData: IToolData = { id: TaskCompleteToolId, displayName: 'Task Complete', modelDescription: - 'Signal that the user\'s task is fully done. Call this only after you have made all changes, ' + - 'verified they work (e.g. no compile errors, tests pass if relevant), and are confident nothing remains. ' + + 'Signal that the user\'s task is fully done. Call this only after you have fully completed the task, ' + + 'verified the results, and are confident nothing remains. ' + 'Provide a brief summary of what was accomplished. If the summary is trivial (e.g. answering a question), omit it. ' + - 'Do not restate the summary in your message text — it is shown to the user directly.', + 'Do not restate the summary in your message text — it is shown to the user directly.\n\n' + + 'When to call:\n' + + '- After you have completed ALL requested changes\n' + + '- After verifying results: tests pass, terminal commands succeeded, tool calls returned expected output\n' + + '- After you have confirmed the solution works correctly\n\n' + + 'When NOT to call:\n' + + '- If a terminal command failed or produced unexpected output\n' + + '- If an MCP or external tool call returned an error\n' + + '- If you encountered errors you have not resolved\n' + + '- If there are remaining steps to complete\n' + + '- If you have not verified your changes work\n' + + '- If you are unsure whether the task is fully done', source: ToolDataSource.Internal, inputSchema: { type: 'object', From ce7599ce8c842726bb11e9f3500c994c57670d73 Mon Sep 17 00:00:00 2001 From: justschen Date: Mon, 2 Mar 2026 15:12:54 -0800 Subject: [PATCH 20/25] fix conflict --- .../contrib/chat/browser/widget/input/chatInputPart.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 150e00c607693..43d3898a4f40c 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2190,10 +2190,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const pickerOptions: IChatInputPickerOptions = { getOverflowAnchor: () => this.inputActionsToolbar.getElement(), actionContext: { widget }, - onlyShowIconsForDefaultActions: observableFromEvent( - this._inputEditor.onDidLayoutChange, - (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 300 /* Threshold for showing icon-only mode in primary toolbar pickers */ - ).recomputeInitiallyAndOnChange(this._store), hideChevrons: derived(reader => this._stableInputPartWidth.read(reader) < 400), hoverPosition: { forcePosition: true, From 5561c5222c28180fd937829b3272900285104f22 Mon Sep 17 00:00:00 2001 From: justschen Date: Mon, 2 Mar 2026 19:25:31 -0800 Subject: [PATCH 21/25] enterprise restrictions --- .../tools/languageModelToolsService.ts | 19 ++++++-- .../input/permissionPickerActionItem.ts | 20 ++++++--- .../tools/builtinTools/askQuestionsTool.ts | 44 +++++++++++++++++++ .../tools/builtinTools/taskCompleteTool.ts | 13 +++--- 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts index c1875ddc48e14..1ac0a6ac178f0 100644 --- a/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/browser/tools/languageModelToolsService.ts @@ -996,6 +996,15 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo }); } + /** + * Returns true if enterprise policy has explicitly disabled the global auto-approve setting. + * When this is the case, Bypass Approvals and Autopilot permission levels should not auto-approve tools. + */ + private _isAutoApprovePolicyRestricted(): boolean { + const inspected = this._configurationService.inspect(ChatConfiguration.GlobalAutoApprove); + return inspected.policyValue === false; + } + private getEligibleForAutoApprovalSpecialCase(toolData: IToolData): string | undefined { if (toolData.id === 'vscode_fetchWebPage_internal') { return 'fetch'; @@ -1046,11 +1055,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo return undefined; } - // Auto-Approve All permission level bypasses all tool confirmations + // Auto-Approve All permission level bypasses all tool confirmations, + // unless enterprise policy has explicitly disabled global auto-approve. if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); - if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } @@ -1087,11 +1097,12 @@ export class LanguageModelToolsService extends Disposable implements ILanguageMo } private async shouldAutoConfirmPostExecution(toolId: string, runsInWorkspace: boolean | undefined, source: ToolDataSource, parameters: unknown, chatSessionResource: URI | undefined, chatRequestId: string | undefined): Promise { - // Auto-Approve All permission level bypasses all post-execution confirmations + // Auto-Approve All permission level bypasses all post-execution confirmations, + // unless enterprise policy has explicitly disabled global auto-approve. if (chatSessionResource) { const model = this._chatService.getSession(chatSessionResource); const request = model?.getRequests().at(-1); - if (isAutoApproveLevel(request?.modeInfo?.permissionLevel)) { + if (isAutoApproveLevel(request?.modeInfo?.permissionLevel) && !this._isAutoApprovePolicyRestricted()) { return { type: ToolConfirmKind.ConfirmationNotNeeded, reason: 'auto-approve-all' }; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index d196b24b5f358..c295a2251490a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -15,8 +15,9 @@ import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; -import { ChatPermissionLevel } from '../../../common/constants.js'; +import { ChatConfiguration, ChatPermissionLevel } from '../../../common/constants.js'; import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; export interface IPermissionPickerDelegate { @@ -33,10 +34,13 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @ITelemetryService telemetryService: ITelemetryService, + @IConfigurationService configurationService: IConfigurationService, ) { + const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; const actionProvider: IActionWidgetDropdownActionProvider = { getActions: () => { const currentLevel = delegate.currentPermissionLevel.get(); + const policyRestricted = isAutoApprovePolicyRestricted(); return [ { ...action, @@ -62,9 +66,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { label: localize('permissions.autoApprove', "Bypass Approvals"), icon: ThemeIcon.fromId(Codicon.warning.id), checked: currentLevel === ChatPermissionLevel.AutoApprove, - tooltip: '', + enabled: !policyRestricted, + tooltip: policyRestricted ? localize('permissions.autoApprove.policyDisabled', "Disabled by enterprise policy") : '', hover: { - content: localize('permissions.autoApprove.description', "Auto-approve all tool calls and retry on errors"), + content: policyRestricted + ? localize('permissions.autoApprove.policyDescription', "Disabled by enterprise policy") + : localize('permissions.autoApprove.description', "Auto-approve all tool calls and retry on errors"), position: pickerOptions.hoverPosition }, run: async () => { @@ -80,9 +87,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { label: localize('permissions.autopilot', "Autopilot"), icon: ThemeIcon.fromId(Codicon.rocket.id), checked: currentLevel === ChatPermissionLevel.Autopilot, - tooltip: '', + enabled: !policyRestricted, + tooltip: policyRestricted ? localize('permissions.autopilot.policyDisabled', "Disabled by enterprise policy") : '', hover: { - content: localize('permissions.autopilot.description', "Auto-approve all tool calls and continue until the task is done"), + content: policyRestricted + ? localize('permissions.autopilot.policyDescription', "Disabled by enterprise policy") + : localize('permissions.autopilot.description', "Auto-approve all tool calls and continue until the task is done"), position: pickerOptions.hoverPosition }, run: async () => { diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts index 57505cb3e0bea..7f7bb156f0707 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/askQuestionsTool.ts @@ -195,8 +195,13 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { } // In autopilot mode, the user is not available — auto-respond instead of blocking. + // Still append a completed carousel so the user can see the auto-selected answers. if (request.modeInfo?.permissionLevel === ChatPermissionLevel.Autopilot) { this.logService.info('[AskQuestionsTool] Autopilot mode: auto-responding to questions'); + const { carousel, idToHeaderMap } = this.toQuestionCarousel(questions); + carousel.data = this.buildAutopilotCarouselAnswers(questions, carousel, idToHeaderMap); + carousel.isUsed = true; + this.chatService.appendProgress(request, carousel); return this.createAutopilotResult(questions); } @@ -508,6 +513,45 @@ export class AskQuestionsTool extends Disposable implements IToolImpl { }; } + /** + * Build carousel answer data keyed by carousel question IDs for rendering + * the completed summary in the UI during autopilot mode. + */ + private buildAutopilotCarouselAnswers(questions: IQuestion[], carousel: ChatQuestionCarouselData, idToHeaderMap: Map): Record { + const data: Record = {}; + // Build reverse map: original header -> internal carousel question ID + const headerToIdMap = new Map(); + for (const [internalId, originalHeader] of idToHeaderMap) { + headerToIdMap.set(originalHeader, internalId); + } + + for (const question of questions) { + const internalId = headerToIdMap.get(question.header); + if (!internalId) { + continue; + } + + const chatQuestion = carousel.questions.find(q => q.id === internalId); + if (!chatQuestion) { + continue; + } + + const recommended = question.options?.find(opt => opt.recommended); + const firstOption = question.options?.[0]; + const selectedLabel = recommended?.label ?? firstOption?.label; + + if (chatQuestion.type === 'text' || !selectedLabel) { + data[internalId] = AUTOPILOT_ASK_USER_RESPONSE; + } else if (chatQuestion.type === 'multiSelect') { + data[internalId] = { selectedValues: [selectedLabel] }; + } else { + data[internalId] = { selectedValue: selectedLabel }; + } + } + + return data; + } + private sendTelemetry(requestId: string | undefined, questionCount: number, answeredCount: number, skippedCount: number, freeTextCount: number, recommendedAvailableCount: number, recommendedSelectedCount: number, duration: number): void { this.telemetryService.publicLog2('askQuestionsToolInvoked', { requestId, diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts index 6ff6314e04e99..74879d30d2111 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/taskCompleteTool.ts @@ -13,6 +13,8 @@ export const TaskCompleteToolId = 'task_complete'; */ export const AUTOPILOT_CONTINUATION_MESSAGE = 'You have not yet marked the task as complete using the task_complete tool. ' + + 'You MUST call task_complete when done — whether the task involved code changes, answering a question, or any other interaction.\n\n' + + 'Do NOT repeat or restate your previous response. Pick up where you left off.\n\n' + 'If you were planning, stop planning and start implementing. ' + 'You are not done until you have fully completed the task.\n\n' + 'IMPORTANT: Do NOT call task_complete if:\n' + @@ -25,21 +27,20 @@ export const TaskCompleteToolData: IToolData = { id: TaskCompleteToolId, displayName: 'Task Complete', modelDescription: - 'Signal that the user\'s task is fully done. Call this only after you have fully completed the task, ' + - 'verified the results, and are confident nothing remains. ' + + 'Signal that the user\'s task is fully done. You MUST call this tool when your work is complete — ' + + 'whether you made code changes, answered a question, or completed any other kind of task. ' + 'Provide a brief summary of what was accomplished. If the summary is trivial (e.g. answering a question), omit it. ' + 'Do not restate the summary in your message text — it is shown to the user directly.\n\n' + 'When to call:\n' + + '- After answering the user\'s question or completing a conversational request\n' + '- After you have completed ALL requested changes\n' + - '- After verifying results: tests pass, terminal commands succeeded, tool calls returned expected output\n' + - '- After you have confirmed the solution works correctly\n\n' + + '- After verifying results: tests pass, terminal commands succeeded, tool calls returned expected output\n\n' + 'When NOT to call:\n' + '- If a terminal command failed or produced unexpected output\n' + '- If an MCP or external tool call returned an error\n' + '- If you encountered errors you have not resolved\n' + '- If there are remaining steps to complete\n' + - '- If you have not verified your changes work\n' + - '- If you are unsure whether the task is fully done', + '- If you have not verified your changes work', source: ToolDataSource.Internal, inputSchema: { type: 'object', From c21c3a2847a46089a1c1cf00847dbe8f090482f0 Mon Sep 17 00:00:00 2001 From: justschen Date: Tue, 3 Mar 2026 16:42:28 -0800 Subject: [PATCH 22/25] revert some stuff, fix sessions window containers --- .../browser/actions/chatExecuteActions.ts | 29 +++++++++++++++++-- src/vs/workbench/contrib/chat/browser/chat.ts | 6 ++++ .../contrib/chat/browser/widget/chatWidget.ts | 1 + .../browser/widget/input/chatInputPart.ts | 20 +++++++++---- .../widgetHosts/viewPane/chatViewPane.ts | 1 + 5 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 9239d2cae453d..67b9fdb6f7013 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -22,6 +22,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ILogService } from '../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { IsSessionsWindowContext } from '../../../../common/contextkeys.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { getModeNameForTelemetry, IChatMode, IChatModeService } from '../../common/chatModes.js'; import { chatVariableLeader } from '../../common/requestParser/chatParserTypes.js'; @@ -469,6 +470,7 @@ export class OpenPermissionPickerAction extends Action2 { ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask), ChatContextKeys.inQuickChat.negate(), + IsSessionsWindowContext.negate(), ) } }); @@ -544,6 +546,17 @@ export class OpenSessionTargetPickerAction extends Action2 { f1: false, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.or(ChatContextKeys.chatSessionIsEmpty, ChatContextKeys.inAgentSessionsWelcome), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ + { + id: MenuId.ChatInput, + order: 0, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.chatSessionIsEmpty, + IsSessionsWindowContext), + group: 'navigation', + }, { id: MenuId.ChatInputSecondary, order: 0, @@ -551,6 +564,7 @@ export class OpenSessionTargetPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), + IsSessionsWindowContext.negate(), ChatContextKeys.chatSessionIsEmpty), group: 'navigation', }, @@ -580,7 +594,7 @@ export class OpenDelegationPickerAction extends Action2 { precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.chatSessionIsEmpty.negate(), ChatContextKeys.currentlyEditingInput.negate(), ChatContextKeys.currentlyEditing.negate()), menu: [ { - id: MenuId.ChatInputSecondary, + id: MenuId.ChatInput, order: 0.5, when: ContextKeyExpr.and( ChatContextKeys.enabled, @@ -614,12 +628,23 @@ export class OpenWorkspacePickerAction extends Action2 { f1: false, precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.inAgentSessionsWelcome), menu: [ + { + id: MenuId.ChatInput, + order: 0.6, + when: ContextKeyExpr.and( + ChatContextKeys.inAgentSessionsWelcome, + ChatContextKeys.chatSessionType.isEqualTo('local'), + IsSessionsWindowContext + ), + group: 'navigation', + }, { id: MenuId.ChatInputSecondary, order: 0.6, when: ContextKeyExpr.and( ChatContextKeys.inAgentSessionsWelcome, - ChatContextKeys.chatSessionType.isEqualTo('local') + ChatContextKeys.chatSessionType.isEqualTo('local'), + IsSessionsWindowContext.negate() ), group: 'navigation', }, diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 7fa8e87b01e28..9b3fc446d6c13 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -289,6 +289,12 @@ export interface IChatWidgetViewOptions { * redirect to a different workspace rather than executing locally. */ submitHandler?: (query: string, mode: ChatModeKind) => Promise; + + /** + * Whether we are running in the sessions window. + * When true, the secondary toolbar (permissions picker) is hidden. + */ + isSessionsWindow?: boolean; } export interface IChatViewViewContext { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts index 8387e3739c5ae..4af985a14919d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -1727,6 +1727,7 @@ export class ChatWidget extends Disposable implements IChatWidget { defaultMode: this.viewOptions.defaultMode, sessionTypePickerDelegate: this.viewOptions.sessionTypePickerDelegate, workspacePickerDelegate: this.viewOptions.workspacePickerDelegate, + isSessionsWindow: this.viewOptions.isSessionsWindow, }; if (this.viewModel?.editing) { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 43d3898a4f40c..687c769be8cee 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -170,6 +170,11 @@ export interface IChatInputPartOptions { * for their chat request. This is useful for empty window contexts. */ workspacePickerDelegate?: IWorkspacePickerDelegate; + /** + * Whether we are running in the sessions window. + * When true, the secondary toolbar (permissions picker) is hidden. + */ + isSessionsWindow?: boolean; } export interface IWorkingSetEntry { @@ -374,7 +379,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private sessionTargetWidget: SessionTypePickerActionItem | undefined; private delegationWidget: DelegationSessionPickerActionItem | undefined; private chatSessionPickerWidgets: Map = new Map(); - private chatSessionPickerContainer!: HTMLElement; + private chatSessionPickerContainer: HTMLElement | undefined; private _lastSessionPickerAction: MenuItemAction | undefined; private readonly _waitForPersistedLanguageModel: MutableDisposable = this._register(new MutableDisposable()); private readonly _chatSessionOptionEmitters: Map> = new Map(); @@ -1988,7 +1993,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-secondary-toolbar@secondaryToolbar', [ dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), ]), - dom.h('.chat-sessionPicker-container@chatSessionPickerContainer'), dom.h('.chat-attachments-container@attachmentsContainer', [ dom.h('.chat-attached-context@attachedContextContainer'), ]), @@ -2015,7 +2019,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-secondary-toolbar@secondaryToolbar', [ dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), ]), - dom.h('.chat-sessionPicker-container@chatSessionPickerContainer'), ]); } this.container = elements.root; @@ -2037,7 +2040,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.attachedContextContainer = elements.attachedContextContainer; const toolbarsContainer = elements.inputToolbars; this.secondaryToolbarContainer = elements.secondaryToolbar; - this.chatSessionPickerContainer = elements.chatSessionPickerContainer; + if (this.options.isSessionsWindow) { + this.secondaryToolbarContainer.style.display = 'none'; + } this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; this.chatGettingStartedTipContainer = elements.chatGettingStartedTipContainer; @@ -2286,6 +2291,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar'); this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext; this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => { + // Update container reference for the pickers + const toolbarElement = this.inputActionsToolbar.getElement(); + // eslint-disable-next-line no-restricted-syntax + const container = toolbarElement.querySelector('.chat-sessionPicker-container'); + this.chatSessionPickerContainer = container as HTMLElement | undefined; if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { this._toolbarRelayoutScheduler.schedule(); } @@ -2318,7 +2328,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge toolbarSide.context = { widget } satisfies IChatExecuteActionContext; } - // Secondary toolbar (session type + permissions) — below the input box + // Secondary toolbar (permissions) — below the input box this.secondaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.secondaryToolbarContainer, MenuId.ChatInputSecondary, { telemetrySource: this.options.menus.telemetrySource, menuOptions: { shouldForwardArgs: true }, diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 4974f0b91ce33..c620909f98b0c 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -540,6 +540,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { supportsChangingModes: true, dndContainer: parent, inputEditorMinLines: this.workbenchEnvironmentService.isSessionsWindow ? 2 : undefined, + isSessionsWindow: this.workbenchEnvironmentService.isSessionsWindow, }, { listForeground: SIDE_BAR_FOREGROUND, From 63ccea84fc6f75f2503ede55ebf1b1d055f6ddfb Mon Sep 17 00:00:00 2001 From: justschen Date: Tue, 3 Mar 2026 17:55:43 -0800 Subject: [PATCH 23/25] fix actions --- .../contrib/chat/browser/actions/chatExecuteActions.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index 67b9fdb6f7013..96bfbe53c78a6 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -470,6 +470,7 @@ export class OpenPermissionPickerAction extends Action2 { ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask), ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.lockedToCodingAgent.negate(), IsSessionsWindowContext.negate(), ) } @@ -662,7 +663,7 @@ export class ChatSessionPrimaryPickerAction extends Action2 { constructor() { super({ id: ChatSessionPrimaryPickerAction.ID, - title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Model Picker"), + title: localize2('interactive.openChatSessionPrimaryPicker.label', "Open Primary Session Picker"), category: CHAT_CATEGORY, f1: false, precondition: ChatContextKeys.enabled, From 85e9eb8f0cfbfbefcf8784ba60cef3b43b861e4d Mon Sep 17 00:00:00 2001 From: justschen Date: Tue, 3 Mar 2026 18:57:27 -0800 Subject: [PATCH 24/25] fix delegate vs. session target --- .../chat/browser/actions/chatExecuteActions.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts index e3d5a5c1ef047..696376a8e982f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts @@ -594,7 +594,19 @@ export class OpenDelegationPickerAction extends Action2 { ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), ChatContextKeys.inQuickChat.negate(), - ChatContextKeys.chatSessionIsEmpty.negate()), + ChatContextKeys.chatSessionIsEmpty.negate(), + IsSessionsWindowContext), + group: 'navigation', + }, + { + id: MenuId.ChatInputSecondary, + order: 0.5, + when: ContextKeyExpr.and( + ChatContextKeys.enabled, + ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat), + ChatContextKeys.inQuickChat.negate(), + ChatContextKeys.chatSessionIsEmpty.negate(), + IsSessionsWindowContext.negate()), group: 'navigation', }, ] From ddbd4ca2484cc698a878a0f9fd109c3cb06096d9 Mon Sep 17 00:00:00 2001 From: justschen Date: Wed, 4 Mar 2026 10:50:01 -0800 Subject: [PATCH 25/25] fix compile + add setting --- .../contrib/chat/browser/chat.contribution.ts | 6 ++++++ .../chat/browser/widget/input/chatInputPart.ts | 4 ++++ .../widget/input/permissionPickerActionItem.ts | 16 ++++++++++------ .../workbench/contrib/chat/common/constants.ts | 1 + .../chat/common/tools/builtinTools/tools.ts | 2 +- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 954b313b84a88..2c38f3c9874da 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -371,6 +371,12 @@ configurationRegistry.registerConfiguration({ scope: ConfigurationScope.APPLICATION_MACHINE, tags: ['experimental', 'advanced'], }, + [ChatConfiguration.AutopilotEnabled]: { + type: 'boolean', + markdownDescription: nls.localize('chat.autopilot.enabled', "Controls whether the Autopilot mode is available in the permissions picker. When enabled, Autopilot auto-approves all tool calls and continues until the task is done."), + default: true, + tags: ['experimental'], + }, [ChatConfiguration.GlobalAutoApprove]: { default: false, markdownDescription: globalAutoApproveDescription.value, diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index e8a490e15b9ae..db90c77372dcd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -2030,6 +2030,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; + if (this.options.isSessionsWindow) { + toolbarsContainer.prepend(this.contextUsageWidgetContainer); + } + // Context usage widget — will be positioned in the toolbar after toolbars are created this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget)); this.contextUsageWidgetContainer.appendChild(this.contextUsageWidget.domNode); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts index c295a2251490a..5f0cc0988d2a3 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/permissionPickerActionItem.ts @@ -37,11 +37,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { @IConfigurationService configurationService: IConfigurationService, ) { const isAutoApprovePolicyRestricted = () => configurationService.inspect(ChatConfiguration.GlobalAutoApprove).policyValue === false; + const isAutopilotEnabled = () => configurationService.getValue(ChatConfiguration.AutopilotEnabled) !== false; const actionProvider: IActionWidgetDropdownActionProvider = { getActions: () => { const currentLevel = delegate.currentPermissionLevel.get(); const policyRestricted = isAutoApprovePolicyRestricted(); - return [ + const actions: IActionWidgetDropdownAction[] = [ { ...action, id: 'chat.permissions.default', @@ -81,10 +82,12 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { } }, } satisfies IActionWidgetDropdownAction, - { + ]; + if (isAutopilotEnabled()) { + actions.push({ ...action, id: 'chat.permissions.autopilot', - label: localize('permissions.autopilot', "Autopilot"), + label: localize('permissions.autopilot', "Autopilot (Preview)"), icon: ThemeIcon.fromId(Codicon.rocket.id), checked: currentLevel === ChatPermissionLevel.Autopilot, enabled: !policyRestricted, @@ -101,8 +104,9 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { this.renderLabel(this.element); } }, - } satisfies IActionWidgetDropdownAction, - ]; + } satisfies IActionWidgetDropdownAction); + } + return actions; } }; @@ -121,7 +125,7 @@ export class PermissionPickerActionItem extends ChatInputPickerActionViewItem { switch (level) { case ChatPermissionLevel.Autopilot: icon = Codicon.rocket; - label = localize('permissions.autopilot.label', "Autopilot"); + label = localize('permissions.autopilot.label', "Autopilot (Preview)"); break; case ChatPermissionLevel.AutoApprove: icon = Codicon.warning; diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index fafd27ac5a5b8..1cc89241a274f 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -54,6 +54,7 @@ export enum ChatConfiguration { ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', GrowthNotificationEnabled = 'chat.growthNotification.enabled', ChatCustomizationMenuEnabled = 'chat.customizationsMenu.enabled', + AutopilotEnabled = 'chat.autopilot.enabled', } /** diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts index 44bad928e7c18..74f61e8b613b1 100644 --- a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts @@ -43,7 +43,7 @@ export class BuiltinToolsContribution extends Disposable implements IWorkbenchCo const taskCompleteTool = instantiationService.createInstance(TaskCompleteTool); this._register(toolsService.registerTool(TaskCompleteToolData, taskCompleteTool)); - + const resolveDebugEventDetailsTool = instantiationService.createInstance(ResolveDebugEventDetailsTool); this._register(toolsService.registerTool(ResolveDebugEventDetailsToolData, resolveDebugEventDetailsTool)); this._register(toolsService.readToolSet.addTool(ResolveDebugEventDetailsToolData));