Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
5310 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { isAncestorOfActiveElement } from '../../../../../base/browser/dom.js';6import { alert } from '../../../../../base/browser/ui/aria/aria.js';7import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';8import { coalesce } from '../../../../../base/common/arrays.js';9import { timeout } from '../../../../../base/common/async.js';10import { Codicon } from '../../../../../base/common/codicons.js';11import { safeIntl } from '../../../../../base/common/date.js';12import { Event } from '../../../../../base/common/event.js';13import { MarkdownString } from '../../../../../base/common/htmlContent.js';14import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';15import { language } from '../../../../../base/common/platform.js';16import { basename } from '../../../../../base/common/resources.js';17import { ThemeIcon } from '../../../../../base/common/themables.js';18import { URI } from '../../../../../base/common/uri.js';19import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';20import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js';21import { IRange } from '../../../../../editor/common/core/range.js';22import { localize, localize2 } from '../../../../../nls.js';23import { Action2, ICommandPaletteOptions, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';24import { ICommandService } from '../../../../../platform/commands/common/commands.js';25import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';26import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';27import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js';28import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';29import { IFileService } from '../../../../../platform/files/common/files.js';30import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';31import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';32import { ILogService } from '../../../../../platform/log/common/log.js';33import { INotificationService } from '../../../../../platform/notification/common/notification.js';34import { IOpenerService } from '../../../../../platform/opener/common/opener.js';35import product from '../../../../../platform/product/common/product.js';36import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';37import { ActiveEditorContext } from '../../../../common/contextkeys.js';38import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';39import { ChatEntitlement, IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';40import { ACTIVE_GROUP, AUX_WINDOW_GROUP } from '../../../../services/editor/common/editorService.js';41import { IHostService } from '../../../../services/host/browser/host.js';42import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';43import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';44import { IViewsService } from '../../../../services/views/common/viewsService.js';45import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';46import { SCMHistoryItemChangeRangeContentProvider, ScmHistoryItemChangeRangeUriFields } from '../../../scm/browser/scmHistoryChatContext.js';47import { ISCMService } from '../../../scm/common/scm.js';48import { IChatAgentResult, IChatAgentService } from '../../common/participants/chatAgents.js';49import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';50import { ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';51import { IChatModel, IChatResponseModel } from '../../common/model/chatModel.js';52import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js';53import { ElicitationState, IChatService, IChatToolInvocation } from '../../common/chatService/chatService.js';54import { ISCMHistoryItemChangeRangeVariableEntry, ISCMHistoryItemChangeVariableEntry } from '../../common/attachments/chatVariableEntries.js';55import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/model/chatViewModel.js';56import { IChatWidgetHistoryService } from '../../common/widget/chatWidgetHistoryService.js';57import { AgentsControlClickBehavior, ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';58import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js';59import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';60import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js';61import { ILanguageModelToolsService, IToolData, IToolSet, isToolSet } from '../../common/tools/languageModelToolsService.js';62import { ChatViewId, IChatWidget, IChatWidgetService } from '../chat.js';63import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';64import { ChatEditorInput, showClearEditingSessionConfirmation } from '../widgetHosts/editor/chatEditorInput.js';65import { convertBufferToScreenshotVariable } from '../attachments/chatScreenshotContext.js';66import { LocalChatSessionUri } from '../../common/model/chatUri.js';6768export const CHAT_CATEGORY = localize2('chat.category', 'Chat');6970export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`;71export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`;72export const ACTION_ID_OPEN_CHAT = 'workbench.action.openChat';73export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open';74export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup';75export const CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID = 'workbench.action.chat.triggerSetupSupportAnonymousAction';76const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle';7778const defaultChat = {79manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',80provider: product.defaultChatAgent?.provider ?? { enterprise: { id: '' } },81completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',82completionsMenuCommand: product.defaultChatAgent?.completionsMenuCommand ?? '',83};8485export interface IChatViewOpenOptions {86/**87* The query for chat.88*/89query: string;90/**91* Whether the query is partial and will await more input from the user.92*/93isPartialQuery?: boolean;94/**95* A list of tools IDs with `canBeReferencedInPrompt` that will be resolved and attached if they exist.96*/97toolIds?: string[];98/**99* Any previous chat requests and responses that should be shown in the chat view.100*/101previousRequests?: IChatViewOpenRequestEntry[];102/**103* Whether a screenshot of the focused window should be taken and attached104*/105attachScreenshot?: boolean;106/**107* A list of file URIs to attach to the chat as context.108*/109attachFiles?: (URI | { uri: URI; range: IRange })[];110/**111* A list of source control history item changes to attach to the chat as context.112*/113attachHistoryItemChanges?: { uri: URI; historyItemId: string }[];114/**115* A list of source control history item change ranges to attach to the chat as context.116*/117attachHistoryItemChangeRanges?: {118start: { uri: URI; historyItemId: string };119end: { uri: URI; historyItemId: string };120}[];121/**122* The mode ID or name to open the chat in.123*/124mode?: ChatModeKind | string;125126/**127* The language model selector to use for the chat.128* An Error will be thrown if there's no match. If there are multiple129* matches, the first match will be used.130*131* Examples:132*133* ```134* {135* id: 'claude-sonnet-4',136* vendor: 'copilot'137* }138* ```139*140* Use `claude-sonnet-4` from any vendor:141*142* ```143* {144* id: 'claude-sonnet-4',145* }146* ```147*/148modelSelector?: ILanguageModelChatSelector;149150/**151* Wait to resolve the command until the chat response reaches a terminal state (complete, error, or pending user confirmation, etc.).152*/153blockOnResponse?: boolean;154155/**156* A list of tool identifiers to include. When specified alone, only these tools will be enabled.157* Identifiers can be tool IDs, tool reference names (`toolReferenceName`),158* toolset IDs, or toolset reference names (`referenceName`).159* When a toolset identifier matches, all tools in that toolset are included.160* Can be combined with `toolsExclude` for fine-grained control.161*/162toolsInclude?: string[];163164/**165* A list of tool identifiers to exclude. When specified alone, all tools except these will be enabled.166* Identifiers can be tool IDs, tool reference names (`toolReferenceName`),167* toolset IDs, or toolset reference names (`referenceName`).168* When a toolset identifier matches, all tools in that toolset are excluded.169* Can be combined with `toolsInclude` - exclusions are applied after inclusions.170* Explicit tool references in `toolsInclude` override toolset exclusions,171* but explicit tool exclusions always win.172*/173toolsExclude?: string[];174}175176export interface IChatViewOpenRequestEntry {177request: string;178response: string;179}180181export const CHAT_CONFIG_MENU_ID = new MenuId('workbench.chat.menu.config');182183const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';184185abstract class OpenChatGlobalAction extends Action2 {186constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: IChatMode) {187super({188...overrides,189icon: Codicon.chatSparkle,190f1: true,191category: CHAT_CATEGORY,192precondition: ContextKeyExpr.and(193ChatContextKeys.Setup.hidden.negate(),194ChatContextKeys.Setup.disabled.negate()195)196});197}198199override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise<IChatAgentResult & { type?: 'confirmation' } | undefined> {200opts = typeof opts === 'string' ? { query: opts } : opts;201202const chatService = accessor.get(IChatService);203const widgetService = accessor.get(IChatWidgetService);204const toolsService = accessor.get(ILanguageModelToolsService);205const hostService = accessor.get(IHostService);206const chatAgentService = accessor.get(IChatAgentService);207const instaService = accessor.get(IInstantiationService);208const commandService = accessor.get(ICommandService);209const chatModeService = accessor.get(IChatModeService);210const fileService = accessor.get(IFileService);211const languageModelService = accessor.get(ILanguageModelsService);212const scmService = accessor.get(ISCMService);213const logService = accessor.get(ILogService);214const configurationService = accessor.get(IConfigurationService);215216let chatWidget = widgetService.lastFocusedWidget;217// When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one.218// Otherwise, open the view.219if (!this.mode || !chatWidget || !isAncestorOfActiveElement(chatWidget.domNode)) {220chatWidget = await widgetService.revealWidget();221}222223if (!chatWidget) {224return;225}226227const switchToMode = (opts?.mode ? chatModeService.findModeByName(opts?.mode) : undefined) ?? this.mode;228if (switchToMode) {229await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);230}231232if (opts?.modelSelector) {233const ids = await languageModelService.selectLanguageModels(opts.modelSelector);234const id = ids.sort().at(0);235if (!id) {236throw new Error(`No language models found matching selector: ${JSON.stringify(opts.modelSelector)}.`);237}238239const model = languageModelService.lookupLanguageModel(id);240if (!model) {241throw new Error(`Language model not loaded: ${id}.`);242}243244chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });245}246247if (opts?.toolsInclude || opts?.toolsExclude) {248const model = chatWidget.input.selectedLanguageModel.get()?.metadata;249const allTools = Array.from(toolsService.getTools(model));250const allToolSets = Array.from(toolsService.getToolSetsForModel(model));251252const result = computeToolEnablementMap({253allTools,254allToolSets,255toolsInclude: opts.toolsInclude,256toolsExclude: opts.toolsExclude,257});258259for (const identifier of result.unknownIdentifiers) {260logService.warn(`Tool filtering: Unknown identifier '${identifier}' - no matching tool or toolset found.`);261}262263chatWidget.input.selectedToolsModel.set(result.enablementMap, true);264}265266if (opts?.previousRequests?.length && chatWidget.viewModel) {267for (const { request, response } of opts.previousRequests) {268chatService.addCompleteRequest(chatWidget.viewModel.sessionResource, request, undefined, 0, { message: response });269}270}271if (opts?.attachScreenshot) {272const screenshot = await hostService.getScreenshot();273if (screenshot) {274chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));275}276}277if (opts?.attachFiles) {278for (const file of opts.attachFiles) {279const uri = file instanceof URI ? file : file.uri;280const range = file instanceof URI ? undefined : file.range;281282if (await fileService.exists(uri)) {283chatWidget.attachmentModel.addFile(uri, range);284}285}286}287if (opts?.attachHistoryItemChanges) {288for (const historyItemChange of opts.attachHistoryItemChanges) {289const repository = scmService.getRepository(URI.file(historyItemChange.uri.path));290const historyProvider = repository?.provider.historyProvider.get();291if (!historyProvider) {292continue;293}294295const historyItem = await historyProvider.resolveHistoryItem(historyItemChange.historyItemId);296if (!historyItem) {297continue;298}299300chatWidget.attachmentModel.addContext({301id: historyItemChange.uri.toString(),302name: `${basename(historyItemChange.uri)}`,303value: historyItemChange.uri,304historyItem: historyItem,305kind: 'scmHistoryItemChange'306} satisfies ISCMHistoryItemChangeVariableEntry);307}308}309if (opts?.attachHistoryItemChangeRanges) {310for (const historyItemChangeRange of opts.attachHistoryItemChangeRanges) {311const repository = scmService.getRepository(URI.file(historyItemChangeRange.end.uri.path));312const historyProvider = repository?.provider.historyProvider.get();313if (!repository || !historyProvider) {314continue;315}316317const [historyItemStart, historyItemEnd] = await Promise.all([318historyProvider.resolveHistoryItem(historyItemChangeRange.start.historyItemId),319historyProvider.resolveHistoryItem(historyItemChangeRange.end.historyItemId),320]);321if (!historyItemStart || !historyItemEnd) {322continue;323}324325const uri = historyItemChangeRange.end.uri.with({326scheme: SCMHistoryItemChangeRangeContentProvider.scheme,327query: JSON.stringify({328repositoryId: repository.id,329start: historyItemStart.id,330end: historyItemChangeRange.end.historyItemId331} satisfies ScmHistoryItemChangeRangeUriFields)332});333334chatWidget.attachmentModel.addContext({335id: uri.toString(),336name: `${basename(uri)}`,337value: uri,338historyItemChangeStart: {339uri: historyItemChangeRange.start.uri,340historyItem: historyItemStart341},342historyItemChangeEnd: {343uri: historyItemChangeRange.end.uri,344historyItem: {345...historyItemEnd,346displayId: historyItemChangeRange.end.historyItemId347}348},349kind: 'scmHistoryItemChangeRange'350} satisfies ISCMHistoryItemChangeRangeVariableEntry);351}352}353354let resp: Promise<IChatResponseModel | undefined> | undefined;355356if (opts?.query) {357358if (opts.isPartialQuery) {359chatWidget.setInput(opts.query);360} else {361if (!chatWidget.viewModel) {362await Event.toPromise(chatWidget.onDidChangeViewModel);363}364await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind);365chatWidget.setInput(opts.query); // wait until the model is restored before setting the input, or it will be cleared when the model is restored366resp = chatWidget.acceptInput();367}368}369370if (opts?.toolIds && opts.toolIds.length > 0) {371for (const toolId of opts.toolIds) {372const tool = toolsService.getTool(toolId);373if (tool) {374chatWidget.attachmentModel.addContext({375id: tool.id,376name: tool.displayName,377fullName: tool.displayName,378value: undefined,379icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined,380kind: 'tool'381});382}383}384}385386chatWidget.focusInput();387388if (opts?.blockOnResponse) {389const response = await resp;390if (response) {391const autoReplyEnabled = configurationService.getValue<boolean>(ChatConfiguration.AutoReply);392await new Promise<void>(resolve => {393const d = response.onDidChange(async () => {394if (response.isComplete) {395d.dispose();396resolve();397return;398}399400const pendingConfirmation = response.isPendingConfirmation.get();401if (pendingConfirmation) {402// Check if the pending confirmation is a question carousel that will be auto-replied.403// Only question carousels are auto-replied; other confirmation types (tool approvals,404// elicitations, etc.) should cause us to resolve immediately.405const hasPendingQuestionCarousel = response.response.value.some(406part => part.kind === 'questionCarousel' && !part.isUsed407);408if (autoReplyEnabled && hasPendingQuestionCarousel) {409// Auto-reply will handle this question carousel, keep waiting410return;411}412d.dispose();413resolve();414}415});416});417418const confirmationInfo = getPendingConfirmationInfo(response);419if (confirmationInfo) {420return { ...response.result, ...confirmationInfo };421}422return { ...response.result };423}424}425426return undefined;427}428429private async handleSwitchToMode(switchToMode: IChatMode, chatWidget: IChatWidget, instaService: IInstantiationService, commandService: ICommandService): Promise<void> {430const currentMode = chatWidget.input.currentModeKind;431432if (switchToMode) {433const model = chatWidget.viewModel?.model;434const chatModeCheck = model ? await instaService.invokeFunction(handleModeSwitch, currentMode, switchToMode.kind, model.getRequests().length, model) : { needToClearSession: false };435if (!chatModeCheck) {436return;437}438chatWidget.input.setChatMode(switchToMode.id);439440if (chatModeCheck.needToClearSession) {441await commandService.executeCommand(ACTION_ID_NEW_CHAT);442}443}444}445}446447async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: ChatModeKind): Promise<void> {448const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode);449if (defaultAgent) {450return;451}452453await Promise.race([454Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {455const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode);456return Boolean(defaultAgent);457})),458timeout(60_000).then(() => { throw new Error('Timed out waiting for default agent'); })459]);460}461462/**463* Information about a pending confirmation in a chat response.464*/465export type IChatPendingConfirmationInfo =466| { type: 'confirmation'; kind: 'toolInvocation'; toolId: string }467| { type: 'confirmation'; kind: 'toolPostApproval'; toolId: string }468| { type: 'confirmation'; kind: 'confirmation'; title: string; data: unknown }469| { type: 'confirmation'; kind: 'questionCarousel'; questions: unknown[] }470| { type: 'confirmation'; kind: 'elicitation'; title: string };471472/**473* Extracts detailed information about the pending confirmation from a chat response.474* Returns undefined if there is no pending confirmation.475*/476function getPendingConfirmationInfo(response: IChatResponseModel): IChatPendingConfirmationInfo | undefined {477for (const part of response.response.value) {478if (part.kind === 'toolInvocation') {479const state = part.state.get();480if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {481return {482type: 'confirmation',483kind: 'toolInvocation',484toolId: part.toolId,485};486}487if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {488return {489type: 'confirmation',490kind: 'toolPostApproval',491toolId: part.toolId,492};493}494}495if (part.kind === 'confirmation' && !part.isUsed) {496return {497type: 'confirmation',498kind: 'confirmation',499title: part.title,500data: part.data,501};502}503if (part.kind === 'questionCarousel' && !part.isUsed) {504return {505type: 'confirmation',506kind: 'questionCarousel',507questions: part.questions,508};509}510if (part.kind === 'elicitation2' && part.state.get() === ElicitationState.Pending) {511const title = part.title;512return {513type: 'confirmation',514kind: 'elicitation',515title: typeof title === 'string' ? title : title.value,516};517}518}519return undefined;520}521522class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction {523constructor() {524super({525id: CHAT_OPEN_ACTION_ID,526title: localize2('openChat', "Open Chat"),527keybinding: {528weight: KeybindingWeight.WorkbenchContrib,529primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,530mac: {531primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI532}533},534menu: [{535id: MenuId.ChatTitleBarMenu,536group: 'a_open',537order: 1538}]539});540}541}542543export function getOpenChatActionIdForMode(mode: IChatMode): string {544return `workbench.action.chat.open${mode.name.get()}`;545}546547export abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {548constructor(mode: IChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {549super({550id: getOpenChatActionIdForMode(mode),551title: localize2('openChatMode', "Open Chat ({0})", mode.label.get()),552keybinding553}, mode);554}555}556557export function registerChatActions() {558registerAction2(PrimaryOpenChatGlobalAction);559registerAction2(class extends ModeOpenChatGlobalAction {560constructor() { super(ChatMode.Ask); }561});562registerAction2(class extends ModeOpenChatGlobalAction {563constructor() {564super(ChatMode.Agent, {565when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentEnabled}`),566weight: KeybindingWeight.WorkbenchContrib,567primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,568linux: {569primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI570}571},);572}573});574registerAction2(class extends ModeOpenChatGlobalAction {575constructor() { super(ChatMode.Edit); }576});577578registerAction2(class ToggleChatAction extends Action2 {579constructor() {580super({581id: TOGGLE_CHAT_ACTION_ID,582title: localize2('toggleChat', "Toggle Chat"),583category: CHAT_CATEGORY584});585}586587async run(accessor: ServicesAccessor) {588const layoutService = accessor.get(IWorkbenchLayoutService);589const viewsService = accessor.get(IViewsService);590const viewDescriptorService = accessor.get(IViewDescriptorService);591const widgetService = accessor.get(IChatWidgetService);592const configurationService = accessor.get(IConfigurationService);593594const chatLocation = viewDescriptorService.getViewLocationById(ChatViewId);595const chatVisible = viewsService.isViewVisible(ChatViewId);596const clickBehavior = configurationService.getValue<AgentsControlClickBehavior>(ChatConfiguration.AgentsControlClickBehavior);597switch (clickBehavior) {598case AgentsControlClickBehavior.Focus:599if (chatLocation === ViewContainerLocation.AuxiliaryBar) {600layoutService.setAuxiliaryBarMaximized(true);601} else {602this.updatePartVisibility(layoutService, chatLocation, true);603}604(await widgetService.revealWidget())?.focusInput();605break;606case AgentsControlClickBehavior.Cycle:607if (chatVisible) {608if (609chatLocation === ViewContainerLocation.AuxiliaryBar &&610!layoutService.isAuxiliaryBarMaximized()611) {612layoutService.setAuxiliaryBarMaximized(true);613(await widgetService.revealWidget())?.focusInput();614} else {615this.updatePartVisibility(layoutService, chatLocation, false);616}617} else {618this.updatePartVisibility(layoutService, chatLocation, true);619(await widgetService.revealWidget())?.focusInput();620}621break;622default:623if (chatVisible) {624this.updatePartVisibility(layoutService, chatLocation, false);625} else {626this.updatePartVisibility(layoutService, chatLocation, true);627(await widgetService.revealWidget())?.focusInput();628}629break;630}631}632633private updatePartVisibility(layoutService: IWorkbenchLayoutService, location: ViewContainerLocation | null, visible: boolean): void {634let part: Parts.PANEL_PART | Parts.SIDEBAR_PART | Parts.AUXILIARYBAR_PART | undefined;635switch (location) {636case ViewContainerLocation.Panel:637part = Parts.PANEL_PART;638break;639case ViewContainerLocation.Sidebar:640part = Parts.SIDEBAR_PART;641break;642case ViewContainerLocation.AuxiliaryBar:643part = Parts.AUXILIARYBAR_PART;644break;645}646647if (part) {648layoutService.setPartHidden(!visible, part);649}650}651});652653654registerAction2(class NewChatEditorAction extends Action2 {655constructor() {656super({657id: ACTION_ID_OPEN_CHAT,658title: localize2('interactiveSession.open', "New Chat Editor"),659icon: Codicon.plus,660f1: true,661category: CHAT_CATEGORY,662precondition: ChatContextKeys.enabled,663keybinding: {664weight: KeybindingWeight.WorkbenchContrib,665primary: KeyMod.CtrlCmd | KeyCode.KeyN,666when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatEditor)667},668menu: [{669id: MenuId.ChatTitleBarMenu,670group: 'b_new',671order: 0672}, {673id: MenuId.ChatNewMenu,674group: '2_new',675order: 2676}, {677id: MenuId.EditorTitle,678group: 'navigation',679when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID),680order: 1681}],682});683}684685async run(accessor: ServicesAccessor) {686const widgetService = accessor.get(IChatWidgetService);687await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), ACTIVE_GROUP, { pinned: true } satisfies IChatEditorOptions);688}689});690691registerAction2(class NewChatWindowAction extends Action2 {692constructor() {693super({694id: `workbench.action.newChatWindow`,695title: localize2('interactiveSession.newChatWindow', "New Chat Window"),696f1: true,697category: CHAT_CATEGORY,698precondition: ChatContextKeys.enabled,699menu: [{700id: MenuId.ChatTitleBarMenu,701group: 'b_new',702order: 1703}, {704id: MenuId.ChatNewMenu,705group: '2_new',706order: 3707}]708});709}710711async run(accessor: ServicesAccessor) {712const widgetService = accessor.get(IChatWidgetService);713await widgetService.openSession(LocalChatSessionUri.getNewSessionUri(), AUX_WINDOW_GROUP, { pinned: true, auxiliary: { compact: true, bounds: { width: 640, height: 640 } } } satisfies IChatEditorOptions);714}715});716717registerAction2(class ClearChatInputHistoryAction extends Action2 {718constructor() {719super({720id: 'workbench.action.chat.clearInputHistory',721title: localize2('interactiveSession.clearHistory.label', "Clear Input History"),722precondition: ChatContextKeys.enabled,723category: CHAT_CATEGORY,724f1: true,725});726}727async run(accessor: ServicesAccessor, ...args: unknown[]) {728const historyService = accessor.get(IChatWidgetHistoryService);729historyService.clearHistory();730}731});732733registerAction2(class FocusChatAction extends EditorAction2 {734constructor() {735super({736id: 'chat.action.focus',737title: localize2('actions.interactiveSession.focus', 'Focus Chat List'),738precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput),739category: CHAT_CATEGORY,740keybinding: [741// On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top742{743when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()),744primary: KeyMod.CtrlCmd | KeyCode.UpArrow,745weight: KeybindingWeight.EditorContrib,746},747// On win/linux, ctrl+up can always focus the chat list748{749when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()),750primary: KeyMod.CtrlCmd | KeyCode.UpArrow,751weight: KeybindingWeight.EditorContrib,752},753{754when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat),755primary: KeyMod.CtrlCmd | KeyCode.DownArrow,756weight: KeybindingWeight.WorkbenchContrib,757}758]759});760}761762runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {763const editorUri = editor.getModel()?.uri;764if (editorUri) {765const widgetService = accessor.get(IChatWidgetService);766widgetService.getWidgetByInputUri(editorUri)?.focusResponseItem();767}768}769});770771registerAction2(class FocusMostRecentlyFocusedChatAction extends EditorAction2 {772constructor() {773super({774id: 'workbench.chat.action.focusLastFocused',775title: localize2('actions.interactiveSession.focusLastFocused', 'Focus Last Focused Chat List Item'),776precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput),777category: CHAT_CATEGORY,778keybinding: [779// On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top780{781when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()),782primary: KeyMod.CtrlCmd | KeyCode.UpArrow | KeyMod.Shift,783weight: KeybindingWeight.EditorContrib + 1,784},785// On win/linux, ctrl+up can always focus the chat list786{787when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()),788primary: KeyMod.CtrlCmd | KeyCode.UpArrow | KeyMod.Shift,789weight: KeybindingWeight.EditorContrib + 1,790},791{792when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat),793primary: KeyMod.CtrlCmd | KeyCode.DownArrow | KeyMod.Shift,794weight: KeybindingWeight.WorkbenchContrib + 1,795}796]797});798}799800runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {801const editorUri = editor.getModel()?.uri;802if (editorUri) {803const widgetService = accessor.get(IChatWidgetService);804widgetService.getWidgetByInputUri(editorUri)?.focusResponseItem(true);805}806}807});808809registerAction2(class FocusChatInputAction extends Action2 {810constructor() {811super({812id: 'workbench.action.chat.focusInput',813title: localize2('interactiveSession.focusInput.label', "Focus Chat Input"),814f1: false,815keybinding: [816{817primary: KeyMod.CtrlCmd | KeyCode.DownArrow,818weight: KeybindingWeight.WorkbenchContrib,819when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat.negate()),820},821{822when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat),823primary: KeyMod.CtrlCmd | KeyCode.UpArrow,824weight: KeybindingWeight.WorkbenchContrib,825}826]827});828}829run(accessor: ServicesAccessor, ...args: unknown[]) {830const widgetService = accessor.get(IChatWidgetService);831widgetService.lastFocusedWidget?.focusInput();832}833});834835registerAction2(class FocusTodosViewAction extends Action2 {836static readonly ID = 'workbench.action.chat.focusTodosView';837838constructor() {839super({840id: FocusTodosViewAction.ID,841title: localize2('interactiveSession.focusTodosView.label', "Agent TODOs: Toggle Focus Between TODOs and Input"),842category: CHAT_CATEGORY,843f1: true,844precondition: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent)),845keybinding: [{846weight: KeybindingWeight.WorkbenchContrib,847primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyT,848when: ContextKeyExpr.or(849ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent)),850ChatContextKeys.inChatTodoList851),852}]853});854}855856run(accessor: ServicesAccessor): void {857const widgetService = accessor.get(IChatWidgetService);858const widget = widgetService.lastFocusedWidget;859860if (!widget || !widget.toggleTodosViewFocus()) {861alert(localize('chat.todoList.focusUnavailable', "No agent todos to focus right now."));862}863}864});865866registerAction2(class FocusQuestionCarouselAction extends Action2 {867static readonly ID = 'workbench.action.chat.focusQuestionCarousel';868869constructor() {870super({871id: FocusQuestionCarouselAction.ID,872title: localize2('interactiveSession.focusQuestionCarousel.label', "Chat: Toggle Focus Between Question and Input"),873category: CHAT_CATEGORY,874f1: true,875precondition: ChatContextKeys.inChatSession,876keybinding: [{877weight: KeybindingWeight.WorkbenchContrib,878primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH,879when: ChatContextKeys.inChatSession,880}]881});882}883884run(accessor: ServicesAccessor): void {885const widgetService = accessor.get(IChatWidgetService);886const widget = widgetService.lastFocusedWidget;887888if (!widget || !widget.toggleQuestionCarouselFocus()) {889alert(localize('chat.questionCarousel.focusUnavailable', "No chat question to focus right now."));890}891}892});893894registerAction2(class FocusTipAction extends Action2 {895static readonly ID = 'workbench.action.chat.focusTip';896897constructor() {898super({899id: FocusTipAction.ID,900title: localize2('interactiveSession.focusTip.label', "Chat: Toggle Focus Between Tip and Input"),901category: CHAT_CATEGORY,902f1: true,903precondition: ChatContextKeys.inChatSession,904keybinding: [{905weight: KeybindingWeight.WorkbenchContrib,906primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Slash,907when: ContextKeyExpr.or(908ChatContextKeys.inChatSession,909ChatContextKeys.inChatTip910),911}]912});913}914915run(accessor: ServicesAccessor): void {916const widgetService = accessor.get(IChatWidgetService);917const widget = widgetService.lastFocusedWidget;918919if (!widget || !widget.toggleTipFocus()) {920alert(localize('chat.tip.focusUnavailable', "No chat tip."));921}922}923});924925registerAction2(class ShowContextUsageAction extends Action2 {926constructor() {927super({928id: 'workbench.action.chat.showContextUsage',929title: localize2('interactiveSession.showContextUsage.label', "Show Context Window Usage"),930category: CHAT_CATEGORY,931f1: true,932precondition: ChatContextKeys.enabled,933});934}935936async run(accessor: ServicesAccessor): Promise<void> {937const widgetService = accessor.get(IChatWidgetService);938const widget = widgetService.lastFocusedWidget ?? (await widgetService.revealWidget());939widget?.input.showContextUsageDetails();940}941});942943const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id));944registerAction2(class extends Action2 {945constructor() {946super({947id: 'workbench.action.chat.manageSettings',948title: localize2('manageChat', "Manage Chat"),949category: CHAT_CATEGORY,950f1: true,951precondition: ContextKeyExpr.and(952ContextKeyExpr.or(953ChatContextKeys.Entitlement.planFree,954ChatContextKeys.Entitlement.planPro,955ChatContextKeys.Entitlement.planProPlus956),957nonEnterpriseCopilotUsers958),959menu: {960id: MenuId.ChatTitleBarMenu,961group: 'y_manage',962order: 1,963when: nonEnterpriseCopilotUsers964}965});966}967968override async run(accessor: ServicesAccessor): Promise<void> {969const openerService = accessor.get(IOpenerService);970openerService.open(URI.parse(defaultChat.manageSettingsUrl));971}972});973974registerAction2(class ShowExtensionsUsingCopilot extends Action2 {975976constructor() {977super({978id: 'workbench.action.chat.showExtensionsUsingCopilot',979title: localize2('showCopilotUsageExtensions', "Show Extensions using Copilot"),980f1: true,981category: EXTENSIONS_CATEGORY,982precondition: ChatContextKeys.enabled983});984}985986override async run(accessor: ServicesAccessor): Promise<void> {987const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);988extensionsWorkbenchService.openSearch(`@contribute:${CopilotUsageExtensionFeatureId}`);989}990});991992registerAction2(class ConfigureCopilotCompletions extends Action2 {993994constructor() {995super({996id: 'workbench.action.chat.configureCodeCompletions',997title: localize2('configureCompletions', "Configure Inline Suggestions..."),998precondition: ContextKeyExpr.and(999ChatContextKeys.Setup.installed,1000ChatContextKeys.Setup.disabled.negate(),1001ChatContextKeys.Setup.untrusted.negate()1002),1003menu: {1004id: MenuId.ChatTitleBarMenu,1005group: 'f_completions',1006order: 10,1007}1008});1009}10101011override async run(accessor: ServicesAccessor): Promise<void> {1012const commandService = accessor.get(ICommandService);1013commandService.executeCommand(defaultChat.completionsMenuCommand);1014}1015});10161017registerAction2(class ShowQuotaExceededDialogAction extends Action2 {10181019constructor() {1020super({1021id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG,1022title: localize('upgradeChat', "Upgrade GitHub Copilot Plan")1023});1024}10251026override async run(accessor: ServicesAccessor) {1027const chatEntitlementService = accessor.get(IChatEntitlementService);1028const commandService = accessor.get(ICommandService);1029const dialogService = accessor.get(IDialogService);1030const telemetryService = accessor.get(ITelemetryService);10311032let message: string;1033const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0;1034const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0;1035if (chatQuotaExceeded && !completionsQuotaExceeded) {1036message = localize('chatQuotaExceeded', "You've reached your monthly chat messages quota. You still have free inline suggestions available.");1037} else if (completionsQuotaExceeded && !chatQuotaExceeded) {1038message = localize('completionsQuotaExceeded', "You've reached your monthly inline suggestions quota. You still have free chat messages available.");1039} else {1040message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat messages and inline suggestions quota.");1041}10421043if (chatEntitlementService.quotas.resetDate) {1044const dateFormatter = chatEntitlementService.quotas.resetDateHasTime ? safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }) : safeIntl.DateTimeFormat(language, { year: 'numeric', month: 'long', day: 'numeric' });1045const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate);1046message = [message, localize('quotaResetDate', "The allowance will reset on {0}.", dateFormatter.value.format(quotaResetDate))].join(' ');1047}10481049const free = chatEntitlementService.entitlement === ChatEntitlement.Free;1050const upgradeToPro = free ? localize('upgradeToPro', "Upgrade to GitHub Copilot Pro (your first 30 days are free) for:\n- Unlimited inline suggestions\n- Unlimited chat messages\n- Access to premium models") : undefined;10511052await dialogService.prompt({1053type: 'none',1054message: localize('copilotQuotaReached', "GitHub Copilot Quota Reached"),1055cancelButton: {1056label: localize('dismiss', "Dismiss"),1057run: () => { /* noop */ }1058},1059buttons: [1060{1061label: free ? localize('upgradePro', "Upgrade to GitHub Copilot Pro") : localize('upgradePlan', "Upgrade GitHub Copilot Plan"),1062run: () => {1063const commandId = 'workbench.action.chat.upgradePlan';1064telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandId, from: 'chat-dialog' });1065commandService.executeCommand(commandId);1066}1067},1068],1069custom: {1070icon: Codicon.copilotWarningLarge,1071markdownDetails: coalesce([1072{ markdown: new MarkdownString(message, true) },1073upgradeToPro ? { markdown: new MarkdownString(upgradeToPro, true) } : undefined1074])1075}1076});1077}1078});10791080registerAction2(class ResetTrustedToolsAction extends Action2 {1081constructor() {1082super({1083id: 'workbench.action.chat.resetTrustedTools',1084title: localize2('resetTrustedTools', "Reset Tool Confirmations"),1085category: CHAT_CATEGORY,1086f1: true,1087precondition: ChatContextKeys.enabled1088});1089}1090override run(accessor: ServicesAccessor): void {1091accessor.get(ILanguageModelToolsConfirmationService).resetToolAutoConfirmation();1092accessor.get(INotificationService).info(localize('resetTrustedToolsSuccess', "Tool confirmation preferences have been reset."));1093}1094});10951096registerAction2(class UpdateInstructionsAction extends Action2 {1097constructor() {1098super({1099id: 'workbench.action.chat.generateInstructions',1100title: localize2('generateInstructions', "Generate Workspace Instructions File"),1101shortTitle: localize2('generateInstructions.short', "Generate Chat Instructions"),1102category: CHAT_CATEGORY,1103icon: Codicon.sparkle,1104f1: true,1105precondition: ChatContextKeys.enabled1106});1107}11081109async run(accessor: ServicesAccessor): Promise<void> {1110const commandService = accessor.get(ICommandService);11111112// Use chat command to open and send the query1113const query = `Analyze this codebase to generate or update \`.github/copilot-instructions.md\` for guiding AI coding agents.11141115Focus on discovering the essential knowledge that would help an AI agents be immediately productive in this codebase. Consider aspects like:1116- The "big picture" architecture that requires reading multiple files to understand - major components, service boundaries, data flows, and the "why" behind structural decisions1117- Critical developer workflows (builds, tests, debugging) especially commands that aren't obvious from file inspection alone1118- Project-specific conventions and patterns that differ from common practices1119- Integration points, external dependencies, and cross-component communication patterns11201121Source existing AI conventions from \`**/{.github/copilot-instructions.md,AGENT.md,AGENTS.md,CLAUDE.md,.cursorrules,.windsurfrules,.clinerules,.cursor/rules/**,.windsurf/rules/**,.clinerules/**,README.md}\` (do one glob search).11221123Guidelines (read more at https://aka.ms/vscode-instructions-docs):1124- If \`.github/copilot-instructions.md\` exists, merge intelligently - preserve valuable content while updating outdated sections1125- Write concise, actionable instructions (~20-50 lines) using markdown structure1126- Include specific examples from the codebase when describing patterns1127- Avoid generic advice ("write tests", "handle errors") - focus on THIS project's specific approaches1128- Document only discoverable patterns, not aspirational practices1129- Reference key files/directories that exemplify important patterns11301131Update \`.github/copilot-instructions.md\` for the user, then ask for feedback on any unclear or incomplete sections to iterate.`;11321133await commandService.executeCommand('workbench.action.chat.open', {1134mode: 'agent',1135query: query,1136});1137}1138});11391140registerAction2(class OpenChatFeatureSettingsAction extends Action2 {1141constructor() {1142super({1143id: 'workbench.action.chat.openFeatureSettings',1144title: localize2('openChatFeatureSettings', "Chat Settings"),1145shortTitle: localize('openChatFeatureSettings.short', "Chat Settings"),1146category: CHAT_CATEGORY,1147f1: true,1148precondition: ChatContextKeys.enabled,1149menu: [{1150id: CHAT_CONFIG_MENU_ID,1151when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),1152order: 15,1153group: '3_configure'1154},1155{1156id: MenuId.ChatWelcomeContext,1157group: '2_settings',1158order: 11159}]1160});1161}11621163override async run(accessor: ServicesAccessor): Promise<void> {1164const preferencesService = accessor.get(IPreferencesService);1165preferencesService.openSettings({ query: '@feature:chat ' });1166}1167});11681169MenuRegistry.appendMenuItem(MenuId.ViewTitle, {1170submenu: CHAT_CONFIG_MENU_ID,1171title: localize2('config.label', "Configure Chat"),1172group: 'navigation',1173when: ContextKeyExpr.equals('view', ChatViewId),1174icon: Codicon.gear,1175order: 61176});1177}11781179export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string {1180if (isRequestVM(item)) {1181return (includeName ? `${item.username}: ` : '') + item.messageText;1182} else {1183return (includeName ? `${item.username}: ` : '') + item.response.toString();1184}1185}11861187export interface IToolFilteringOptions {1188allTools: IToolData[];1189allToolSets: IToolSet[];1190toolsInclude?: string[];1191toolsExclude?: string[];1192}11931194export interface IToolFilteringResult {1195enablementMap: Map<IToolData | IToolSet, boolean>;1196unknownIdentifiers: string[];1197}11981199/**1200* Computes the tool enablement map based on include/exclude filters.1201*1202* Resolution algorithm:1203* 1. If `toolsInclude` is specified, start with only those tools/toolsets enabled1204* 2. If `toolsExclude` is specified, remove those tools/toolsets1205* 3. Explicit tool references in `toolsInclude` override toolset exclusions1206* 4. Explicit tool exclusions always win1207* 5. Toolset enablement is calculated based on whether all member tools are enabled1208*1209* @throws Error if filtering results in zero enabled tools1210*/1211export function computeToolEnablementMap(options: IToolFilteringOptions): IToolFilteringResult {1212const { allTools, allToolSets, toolsInclude, toolsExclude } = options;12131214const enablementMap = new Map<IToolData | IToolSet, boolean>();1215const matchedIdentifiers = new Set<string>();12161217// Helper to check if a tool matches any identifier (by id or toolReferenceName)1218const toolMatches = (tool: IToolData, identifiers: Set<string>): boolean => {1219if (identifiers.has(tool.id)) {1220matchedIdentifiers.add(tool.id);1221return true;1222}1223if (tool.toolReferenceName && identifiers.has(tool.toolReferenceName)) {1224matchedIdentifiers.add(tool.toolReferenceName);1225return true;1226}1227return false;1228};12291230// Helper to check if a toolset matches any identifier (by id or referenceName)1231const toolSetMatches = (toolSet: IToolSet, identifiers: Set<string>): boolean => {1232if (identifiers.has(toolSet.id)) {1233matchedIdentifiers.add(toolSet.id);1234return true;1235}1236if (identifiers.has(toolSet.referenceName)) {1237matchedIdentifiers.add(toolSet.referenceName);1238return true;1239}1240return false;1241};12421243// Track which tools are explicitly referenced in toolsInclude1244const explicitlyIncludedTools = new Set<IToolData>();12451246// Step 1: Build initial set based on toolsInclude1247if (toolsInclude) {1248const includeSet = new Set(toolsInclude);12491250// First, process toolsets - if a toolset matches, enable all its tools1251for (const toolSet of allToolSets) {1252if (toolSetMatches(toolSet, includeSet)) {1253for (const tool of toolSet.getTools()) {1254enablementMap.set(tool, true);1255}1256}1257}12581259// Then process individual tools1260for (const tool of allTools) {1261if (toolMatches(tool, includeSet)) {1262enablementMap.set(tool, true);1263explicitlyIncludedTools.add(tool);1264} else if (!enablementMap.has(tool)) {1265enablementMap.set(tool, false);1266}1267}1268// Also process tools from toolsets that may not be in allTools1269for (const toolSet of allToolSets) {1270for (const tool of toolSet.getTools()) {1271if (toolMatches(tool, includeSet)) {1272enablementMap.set(tool, true);1273explicitlyIncludedTools.add(tool);1274} else if (!enablementMap.has(tool)) {1275enablementMap.set(tool, false);1276}1277}1278}1279} else {1280// No toolsInclude specified - start with all tools enabled1281for (const tool of allTools) {1282enablementMap.set(tool, true);1283}1284for (const toolSet of allToolSets) {1285for (const tool of toolSet.getTools()) {1286enablementMap.set(tool, true);1287}1288}1289}12901291// Step 2: Remove tools matching toolsExclude1292if (toolsExclude) {1293const excludeSet = new Set(toolsExclude);12941295// First, process toolsets - if a toolset matches, disable all its tools1296// (unless explicitly included as individual tools)1297for (const toolSet of allToolSets) {1298if (toolSetMatches(toolSet, excludeSet)) {1299for (const tool of toolSet.getTools()) {1300// Explicit tool reference overrides toolset exclusion1301if (!explicitlyIncludedTools.has(tool)) {1302enablementMap.set(tool, false);1303}1304}1305}1306}13071308// Then process individual tools - explicit exclusion always wins1309for (const tool of allTools) {1310if (toolMatches(tool, excludeSet)) {1311enablementMap.set(tool, false);1312}1313}1314for (const toolSet of allToolSets) {1315for (const tool of toolSet.getTools()) {1316if (toolMatches(tool, excludeSet)) {1317enablementMap.set(tool, false);1318}1319}1320}1321}13221323// Collect unknown identifiers1324const allIdentifiers = new Set([...(toolsInclude ?? []), ...(toolsExclude ?? [])]);1325const unknownIdentifiers: string[] = [];1326for (const identifier of allIdentifiers) {1327if (!matchedIdentifiers.has(identifier)) {1328unknownIdentifiers.push(identifier);1329}1330}13311332// Validate at least one tool is enabled1333const enabledToolCount = Array.from(enablementMap.entries()).filter(([item, enabled]) => enabled && !isToolSet(item)).length;1334if (enabledToolCount === 0) {1335throw new Error('Tool filtering resulted in zero enabled tools. At least one tool must be enabled.');1336}13371338// Calculate toolset enablement based on whether all member tools are enabled1339for (const toolSet of allToolSets) {1340const toolSetTools = Array.from(toolSet.getTools());1341const allToolsEnabled = toolSetTools.length > 0 && toolSetTools.every(t => enablementMap.get(t) === true);1342enablementMap.set(toolSet, allToolsEnabled);1343}13441345return { enablementMap, unknownIdentifiers };1346}134713481349/**1350* Returns whether we can continue clearing/switching chat sessions, false to cancel.1351*/1352export async function handleCurrentEditingSession(model: IChatModel, phrase: string | undefined, dialogService: IDialogService): Promise<boolean> {1353return showClearEditingSessionConfirmation(model, dialogService, { messageOverride: phrase });1354}13551356/**1357* Returns whether we can switch the agent, based on whether the user had to agree to clear the session, false to cancel.1358*/1359export async function handleModeSwitch(1360accessor: ServicesAccessor,1361fromMode: ChatModeKind,1362toMode: ChatModeKind,1363requestCount: number,1364model: IChatModel | undefined,1365): Promise<false | { needToClearSession: boolean }> {1366if (!model?.editingSession || fromMode === toMode) {1367return { needToClearSession: false };1368}13691370const configurationService = accessor.get(IConfigurationService);1371const dialogService = accessor.get(IDialogService);1372const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit)) && requestCount > 0;1373if (needToClearEdits) {1374// If not using edits2 and switching into or out of edit mode, ask to discard the session1375const phrase = localize('switchMode.confirmPhrase', "Switching agents will end your current edit session.");13761377const currentEdits = model.editingSession.entries.get();1378const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);1379if (undecidedEdits.length > 0) {1380if (!await handleCurrentEditingSession(model, phrase, dialogService)) {1381return false;1382}13831384return { needToClearSession: true };1385} else {1386const confirmation = await dialogService.confirm({1387title: localize('agent.newSession', "Start new session?"),1388message: localize('agent.newSessionMessage', "Changing the agent will end your current edit session. Would you like to change the agent?"),1389primaryButton: localize('agent.newSession.confirm', "Yes"),1390type: 'info'1391});1392if (!confirmation.confirmed) {1393return false;1394}13951396return { needToClearSession: true };1397}1398}13991400return { needToClearSession: false };1401}14021403export interface IClearEditingSessionConfirmationOptions {1404titleOverride?: string;1405messageOverride?: string;1406isArchiveAction?: boolean;1407}140814091410// --- Chat Submenus in various Components14111412MenuRegistry.appendMenuItem(MenuId.EditorContext, {1413submenu: MenuId.ChatTextEditorMenu,1414group: '1_chat',1415order: 5,1416title: localize('generateCode', "Generate Code"),1417when: ContextKeyExpr.and(1418ChatContextKeys.Setup.hidden.negate(),1419ChatContextKeys.Setup.disabled.negate()1420)1421});14221423// --- Chat Default Visibility14241425registerAction2(class ToggleDefaultVisibilityAction extends Action2 {1426constructor() {1427super({1428id: 'workbench.action.chat.toggleDefaultVisibility',1429title: localize2('chat.toggleDefaultVisibility.label', "Show View by Default"),1430toggled: ContextKeyExpr.equals('config.workbench.secondarySideBar.defaultVisibility', 'hidden').negate(),1431f1: false,1432menu: {1433id: MenuId.ViewTitle,1434when: ContextKeyExpr.and(1435ContextKeyExpr.equals('view', ChatViewId),1436ChatContextKeys.panelLocation.isEqualTo(ViewContainerLocation.AuxiliaryBar),1437),1438order: 0,1439group: '5_configure'1440},1441});1442}14431444async run(accessor: ServicesAccessor) {1445const configurationService = accessor.get(IConfigurationService);14461447const currentValue = configurationService.getValue<'hidden' | unknown>('workbench.secondarySideBar.defaultVisibility');1448configurationService.updateValue('workbench.secondarySideBar.defaultVisibility', currentValue !== 'hidden' ? 'hidden' : 'visible');1449}1450});14511452registerAction2(class EditToolApproval extends Action2 {1453constructor() {1454super({1455id: 'workbench.action.chat.editToolApproval',1456title: localize2('chat.editToolApproval.label', "Manage Tool Approval"),1457metadata: {1458description: localize2('chat.editToolApproval.description', "Edit/manage the tool approval and confirmation preferences for AI chat agents."),1459},1460precondition: ChatContextKeys.enabled,1461f1: true,1462category: CHAT_CATEGORY,1463});1464}14651466async run(accessor: ServicesAccessor, scope?: 'workspace' | 'profile' | 'session'): Promise<void> {1467const confirmationService = accessor.get(ILanguageModelToolsConfirmationService);1468const toolsService = accessor.get(ILanguageModelToolsService);1469confirmationService.manageConfirmationPreferences([...toolsService.getAllToolsIncludingDisabled()], scope ? { defaultScope: scope } : undefined);1470}1471});147214731474