Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
3296 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 { toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js';7import { coalesce } from '../../../../../base/common/arrays.js';8import { timeout } from '../../../../../base/common/async.js';9import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';10import { Codicon } from '../../../../../base/common/codicons.js';11import { fromNowByDay, 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 { Disposable, DisposableStore, markAsSingleton } from '../../../../../base/common/lifecycle.js';16import { MarshalledId } from '../../../../../base/common/marshallingIds.js';17import { language } from '../../../../../base/common/platform.js';18import { ThemeIcon } from '../../../../../base/common/themables.js';19import { URI } from '../../../../../base/common/uri.js';20import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';21import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js';22import { Position } from '../../../../../editor/common/core/position.js';23import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js';24import { localize, localize2 } from '../../../../../nls.js';25import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';26import { DropdownWithPrimaryActionViewItem } from '../../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';27import { getContextMenuActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js';28import { Action2, ICommandPaletteOptions, IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2, SubmenuItemAction } from '../../../../../platform/actions/common/actions.js';29import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';30import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';31import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';32import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js';33import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';34import { IFileService } from '../../../../../platform/files/common/files.js';35import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';36import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';37import { INotificationService } from '../../../../../platform/notification/common/notification.js';38import { IOpenerService } from '../../../../../platform/opener/common/opener.js';39import product from '../../../../../platform/product/common/product.js';40import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';41import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';42import { ToggleTitleBarConfigAction } from '../../../../browser/parts/titlebar/titlebarActions.js';43import { ActiveEditorContext, IsCompactTitleBarContext } from '../../../../common/contextkeys.js';44import { IWorkbenchContribution } from '../../../../common/contributions.js';45import { IViewDescriptorService, ViewContainerLocation } from '../../../../common/views.js';46import { GroupDirection, IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';47import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../services/editor/common/editorService.js';48import { IHostService } from '../../../../services/host/browser/host.js';49import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js';50import { IViewsService } from '../../../../services/views/common/viewsService.js';51import { IPreferencesService } from '../../../../services/preferences/common/preferences.js';52import { EXTENSIONS_CATEGORY, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';53import { IChatAgentResult, IChatAgentService } from '../../common/chatAgents.js';54import { ChatContextKeys } from '../../common/chatContextKeys.js';55import { IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js';56import { ChatEntitlement, IChatEntitlementService } from '../../common/chatEntitlementService.js';57import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js';58import { extractAgentAndCommand } from '../../common/chatParserTypes.js';59import { IChatDetail, IChatService } from '../../common/chatService.js';60import { IChatSessionItem, IChatSessionsService } from '../../common/chatSessionsService.js';61import { ChatSessionUri } from '../../common/chatUri.js';62import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../common/chatViewModel.js';63import { IChatWidgetHistoryService } from '../../common/chatWidgetHistoryService.js';64import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';65import { CopilotUsageExtensionFeatureId } from '../../common/languageModelStats.js';66import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';67import { ChatViewId, IChatWidget, IChatWidgetService, showChatView, showCopilotView } from '../chat.js';68import { IChatEditorOptions } from '../chatEditor.js';69import { ChatEditorInput, shouldShowClearEditingSessionConfirmation, showClearEditingSessionConfirmation } from '../chatEditorInput.js';70import { VIEWLET_ID } from '../chatSessions.js';71import { ChatViewPane } from '../chatViewPane.js';72import { convertBufferToScreenshotVariable } from '../contrib/screenshot.js';73import { clearChatEditor } from './chatClear.js';74import { ILanguageModelChatSelector, ILanguageModelsService } from '../../common/languageModels.js';75import { IChatResponseModel } from '../../common/chatModel.js';76import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';77import { mainWindow } from '../../../../../base/browser/window.js';7879export const CHAT_CATEGORY = localize2('chat.category', 'Chat');8081export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`;82export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`;83export const CHAT_OPEN_ACTION_ID = 'workbench.action.chat.open';84export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup';85const TOGGLE_CHAT_ACTION_ID = 'workbench.action.chat.toggle';86const CHAT_CLEAR_HISTORY_ACTION_ID = 'workbench.action.chat.clearHistory';8788export interface IChatViewOpenOptions {89/**90* The query for chat.91*/92query: string;93/**94* Whether the query is partial and will await more input from the user.95*/96isPartialQuery?: boolean;97/**98* A list of tools IDs with `canBeReferencedInPrompt` that will be resolved and attached if they exist.99*/100toolIds?: string[];101/**102* Any previous chat requests and responses that should be shown in the chat view.103*/104previousRequests?: IChatViewOpenRequestEntry[];105/**106* Whether a screenshot of the focused window should be taken and attached107*/108attachScreenshot?: boolean;109/**110* A list of file URIs to attach to the chat as context.111*/112attachFiles?: URI[];113/**114* The mode ID or name to open the chat in.115*/116mode?: ChatModeKind | string;117118/**119* The language model selector to use for the chat.120* An Error will be thrown if there's no match. If there are multiple121* matches, the first match will be used.122*123* Examples:124*125* ```126* {127* id: 'claude-sonnet-4',128* vendor: 'copilot'129* }130* ```131*132* Use `claude-sonnet-4` from any vendor:133*134* ```135* {136* id: 'claude-sonnet-4',137* }138* ```139*/140modelSelector?: ILanguageModelChatSelector;141142/**143* Wait to resolve the command until the chat response reaches a terminal state (complete, error, or pending user confirmation, etc.).144*/145blockOnResponse?: boolean;146}147148export interface IChatViewOpenRequestEntry {149request: string;150response: string;151}152153export const CHAT_CONFIG_MENU_ID = new MenuId('workbench.chat.menu.config');154155const OPEN_CHAT_QUOTA_EXCEEDED_DIALOG = 'workbench.action.chat.openQuotaExceededDialog';156157abstract class OpenChatGlobalAction extends Action2 {158constructor(overrides: Pick<ICommandPaletteOptions, 'keybinding' | 'title' | 'id' | 'menu'>, private readonly mode?: IChatMode) {159super({160...overrides,161icon: Codicon.chatSparkle,162f1: true,163category: CHAT_CATEGORY,164precondition: ContextKeyExpr.and(165ChatContextKeys.Setup.hidden.negate(),166ChatContextKeys.Setup.disabled.negate()167)168});169}170171override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise<IChatAgentResult & { type?: 'confirmation' } | undefined> {172opts = typeof opts === 'string' ? { query: opts } : opts;173174const chatService = accessor.get(IChatService);175const widgetService = accessor.get(IChatWidgetService);176const toolsService = accessor.get(ILanguageModelToolsService);177const viewsService = accessor.get(IViewsService);178const hostService = accessor.get(IHostService);179const chatAgentService = accessor.get(IChatAgentService);180const instaService = accessor.get(IInstantiationService);181const commandService = accessor.get(ICommandService);182const chatModeService = accessor.get(IChatModeService);183const fileService = accessor.get(IFileService);184const languageModelService = accessor.get(ILanguageModelsService);185186let chatWidget = widgetService.lastFocusedWidget;187// When this was invoked to switch to a mode via keybinding, and some chat widget is focused, use that one.188// Otherwise, open the view.189if (!this.mode || !chatWidget || !isAncestorOfActiveElement(chatWidget.domNode)) {190chatWidget = await showChatView(viewsService);191}192193if (!chatWidget) {194return;195}196197const switchToMode = (opts?.mode ? chatModeService.findModeByName(opts?.mode) : undefined) ?? this.mode;198if (switchToMode) {199await this.handleSwitchToMode(switchToMode, chatWidget, instaService, commandService);200}201202if (opts?.modelSelector) {203const ids = await languageModelService.selectLanguageModels(opts.modelSelector, false);204const id = ids.sort().at(0);205if (!id) {206throw new Error(`No language models found matching selector: ${JSON.stringify(opts.modelSelector)}.`);207}208209const model = languageModelService.lookupLanguageModel(id);210if (!model) {211throw new Error(`Language model not loaded: ${id}.`);212}213214chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });215}216217if (opts?.previousRequests?.length && chatWidget.viewModel) {218for (const { request, response } of opts.previousRequests) {219chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response });220}221}222if (opts?.attachScreenshot) {223const screenshot = await hostService.getScreenshot();224if (screenshot) {225chatWidget.attachmentModel.addContext(convertBufferToScreenshotVariable(screenshot));226}227}228if (opts?.attachFiles) {229for (const file of opts.attachFiles) {230if (await fileService.exists(file)) {231chatWidget.attachmentModel.addFile(file);232}233}234}235236let resp: Promise<IChatResponseModel | undefined> | undefined;237238if (opts?.query) {239chatWidget.setInput(opts.query);240241if (!opts.isPartialQuery) {242await chatWidget.waitForReady();243await waitForDefaultAgent(chatAgentService, chatWidget.input.currentModeKind);244resp = chatWidget.acceptInput();245}246}247248if (opts?.toolIds && opts.toolIds.length > 0) {249for (const toolId of opts.toolIds) {250const tool = toolsService.getTool(toolId);251if (tool) {252chatWidget.attachmentModel.addContext({253id: tool.id,254name: tool.displayName,255fullName: tool.displayName,256value: undefined,257icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined,258kind: 'tool'259});260}261}262}263264chatWidget.focusInput();265266if (opts?.blockOnResponse) {267const response = await resp;268if (response) {269await new Promise<void>(resolve => {270const d = response.onDidChange(async () => {271if (response.isComplete || response.isPendingConfirmation.get()) {272d.dispose();273resolve();274}275});276});277278return { ...response.result, type: response.isPendingConfirmation.get() ? 'confirmation' : undefined };279}280}281282return undefined;283}284285private async handleSwitchToMode(switchToMode: IChatMode, chatWidget: IChatWidget, instaService: IInstantiationService, commandService: ICommandService): Promise<void> {286const currentMode = chatWidget.input.currentModeKind;287288if (switchToMode) {289const editingSession = chatWidget.viewModel?.model.editingSession;290const requestCount = chatWidget.viewModel?.model.getRequests().length ?? 0;291const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, currentMode, switchToMode.kind, requestCount, editingSession);292if (!chatModeCheck) {293return;294}295chatWidget.input.setChatMode(switchToMode.id);296297if (chatModeCheck.needToClearSession) {298await commandService.executeCommand(ACTION_ID_NEW_CHAT);299}300}301}302}303304async function waitForDefaultAgent(chatAgentService: IChatAgentService, mode: ChatModeKind): Promise<void> {305const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, mode);306if (defaultAgent) {307return;308}309310await Promise.race([311Event.toPromise(Event.filter(chatAgentService.onDidChangeAgents, () => {312const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, mode);313return Boolean(defaultAgent);314})),315timeout(60_000).then(() => { throw new Error('Timed out waiting for default agent'); })316]);317}318319class PrimaryOpenChatGlobalAction extends OpenChatGlobalAction {320constructor() {321super({322id: CHAT_OPEN_ACTION_ID,323title: localize2('openChat', "Open Chat"),324keybinding: {325weight: KeybindingWeight.WorkbenchContrib,326primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,327mac: {328primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI329}330},331menu: [{332id: MenuId.ChatTitleBarMenu,333group: 'a_open',334order: 1335}]336});337}338}339340export function getOpenChatActionIdForMode(mode: IChatMode): string {341return `workbench.action.chat.open${mode.name}`;342}343344abstract class ModeOpenChatGlobalAction extends OpenChatGlobalAction {345constructor(mode: IChatMode, keybinding?: ICommandPaletteOptions['keybinding']) {346super({347id: getOpenChatActionIdForMode(mode),348title: localize2('openChatMode', "Open Chat ({0})", mode.label),349keybinding350}, mode);351}352}353354export function registerChatActions() {355registerAction2(PrimaryOpenChatGlobalAction);356registerAction2(class extends ModeOpenChatGlobalAction {357constructor() { super(ChatMode.Ask); }358});359registerAction2(class extends ModeOpenChatGlobalAction {360constructor() {361super(ChatMode.Agent, {362when: ContextKeyExpr.has(`config.${ChatConfiguration.AgentEnabled}`),363weight: KeybindingWeight.WorkbenchContrib,364primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,365linux: {366primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI367}368},);369}370});371registerAction2(class extends ModeOpenChatGlobalAction {372constructor() { super(ChatMode.Edit); }373});374375registerAction2(class ToggleChatAction extends Action2 {376constructor() {377super({378id: TOGGLE_CHAT_ACTION_ID,379title: localize2('toggleChat', "Toggle Chat"),380category: CHAT_CATEGORY381});382}383384async run(accessor: ServicesAccessor) {385const layoutService = accessor.get(IWorkbenchLayoutService);386const viewsService = accessor.get(IViewsService);387const viewDescriptorService = accessor.get(IViewDescriptorService);388389const chatLocation = viewDescriptorService.getViewLocationById(ChatViewId);390391if (viewsService.isViewVisible(ChatViewId)) {392this.updatePartVisibility(layoutService, chatLocation, false);393} else {394this.updatePartVisibility(layoutService, chatLocation, true);395(await showCopilotView(viewsService, layoutService))?.focusInput();396}397}398399private updatePartVisibility(layoutService: IWorkbenchLayoutService, location: ViewContainerLocation | null, visible: boolean): void {400let part: Parts.PANEL_PART | Parts.SIDEBAR_PART | Parts.AUXILIARYBAR_PART | undefined;401switch (location) {402case ViewContainerLocation.Panel:403part = Parts.PANEL_PART;404break;405case ViewContainerLocation.Sidebar:406part = Parts.SIDEBAR_PART;407break;408case ViewContainerLocation.AuxiliaryBar:409part = Parts.AUXILIARYBAR_PART;410break;411}412413if (part) {414layoutService.setPartHidden(!visible, part);415}416}417});418419registerAction2(class ChatHistoryAction extends Action2 {420constructor() {421super({422id: `workbench.action.chat.history`,423title: localize2('chat.history.label', "Show Chats..."),424menu: [425{426id: MenuId.ViewTitle,427when: ContextKeyExpr.and(428ContextKeyExpr.equals('view', ChatViewId),429ChatContextKeys.inEmptyStateWithHistoryEnabled.negate()430),431group: 'navigation',432order: 2433},434{435id: MenuId.EditorTitle,436when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID),437},438],439category: CHAT_CATEGORY,440icon: Codicon.history,441f1: true,442precondition: ChatContextKeys.enabled443});444}445446private showLegacyPicker = async (447chatService: IChatService,448quickInputService: IQuickInputService,449commandService: ICommandService,450editorService: IEditorService,451view: ChatViewPane452) => {453const clearChatHistoryButton: IQuickInputButton = {454iconClass: ThemeIcon.asClassName(Codicon.clearAll),455tooltip: localize('interactiveSession.history.clear', "Clear All Workspace Chats"),456};457458const openInEditorButton: IQuickInputButton = {459iconClass: ThemeIcon.asClassName(Codicon.file),460tooltip: localize('interactiveSession.history.editor', "Open in Editor"),461};462const deleteButton: IQuickInputButton = {463iconClass: ThemeIcon.asClassName(Codicon.x),464tooltip: localize('interactiveSession.history.delete', "Delete"),465};466const renameButton: IQuickInputButton = {467iconClass: ThemeIcon.asClassName(Codicon.pencil),468tooltip: localize('chat.history.rename', "Rename"),469};470471interface IChatPickerItem extends IQuickPickItem {472chat: IChatDetail;473}474475const getPicks = async () => {476const items = await chatService.getHistory();477items.sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0));478479let lastDate: string | undefined = undefined;480const picks = items.flatMap((i): [IQuickPickSeparator | undefined, IChatPickerItem] => {481const timeAgoStr = fromNowByDay(i.lastMessageDate, true, true);482const separator: IQuickPickSeparator | undefined = timeAgoStr !== lastDate ? {483type: 'separator', label: timeAgoStr,484} : undefined;485lastDate = timeAgoStr;486return [487separator,488{489label: i.title,490description: i.isActive ? `(${localize('currentChatLabel', 'current')})` : '',491chat: i,492buttons: i.isActive ? [renameButton] : [493renameButton,494openInEditorButton,495deleteButton,496]497}498];499});500501return coalesce(picks);502};503504const store = new (DisposableStore as { new(): DisposableStore })();505const picker = store.add(quickInputService.createQuickPick<IChatPickerItem>({ useSeparators: true }));506picker.title = localize('interactiveSession.history.title', "Workspace Chat History");507picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat");508picker.buttons = [clearChatHistoryButton];509const picks = await getPicks();510picker.items = picks;511store.add(picker.onDidTriggerButton(async button => {512if (button === clearChatHistoryButton) {513await commandService.executeCommand(CHAT_CLEAR_HISTORY_ACTION_ID);514}515}));516store.add(picker.onDidTriggerItemButton(async context => {517if (context.button === openInEditorButton) {518const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true };519editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP);520picker.hide();521} else if (context.button === deleteButton) {522chatService.removeHistoryEntry(context.item.chat.sessionId);523picker.items = await getPicks();524} else if (context.button === renameButton) {525const title = await quickInputService.input({ title: localize('newChatTitle', "New chat title"), value: context.item.chat.title });526if (title) {527chatService.setChatSessionTitle(context.item.chat.sessionId, title);528}529530// The quick input hides the picker, it gets disposed, so we kick it off from scratch531await this.showLegacyPicker(chatService, quickInputService, commandService, editorService, view);532}533}));534store.add(picker.onDidAccept(async () => {535try {536const item = picker.selectedItems[0];537const sessionId = item.chat.sessionId;538await view.loadSession(sessionId);539} finally {540picker.hide();541}542}));543store.add(picker.onDidHide(() => store.dispose()));544545picker.show();546};547548private showIntegratedPicker = async (549chatService: IChatService,550quickInputService: IQuickInputService,551commandService: ICommandService,552editorService: IEditorService,553chatWidgetService: IChatWidgetService,554view: ChatViewPane,555chatSessionsService: IChatSessionsService,556contextKeyService: IContextKeyService,557menuService: IMenuService,558showAllChats: boolean = false,559showAllAgents: boolean = false560) => {561const clearChatHistoryButton: IQuickInputButton = {562iconClass: ThemeIcon.asClassName(Codicon.clearAll),563tooltip: localize('interactiveSession.history.clear', "Clear All Workspace Chats"),564};565566const openInEditorButton: IQuickInputButton = {567iconClass: ThemeIcon.asClassName(Codicon.file),568tooltip: localize('interactiveSession.history.editor', "Open in Editor"),569};570const deleteButton: IQuickInputButton = {571iconClass: ThemeIcon.asClassName(Codicon.x),572tooltip: localize('interactiveSession.history.delete', "Delete"),573};574const renameButton: IQuickInputButton = {575iconClass: ThemeIcon.asClassName(Codicon.pencil),576tooltip: localize('chat.history.rename', "Rename"),577};578579interface IChatPickerItem extends IQuickPickItem {580chat: IChatDetail;581}582583interface ICodingAgentPickerItem extends IChatPickerItem {584id?: string;585session?: { providerType: string; session: IChatSessionItem };586uri?: URI;587}588589const getPicks = async (showAllChats: boolean = false, showAllAgents: boolean = false) => {590// Fast picks: Get cached/immediate items first591const cachedItems = await chatService.getHistory();592cachedItems.sort((a, b) => (b.lastMessageDate ?? 0) - (a.lastMessageDate ?? 0));593594const allFastPickItems: IChatPickerItem[] = cachedItems.map((i) => {595const timeAgoStr = fromNowByDay(i.lastMessageDate, true, true);596const currentLabel = i.isActive ? localize('currentChatLabel', 'current') : '';597const description = currentLabel ? `${timeAgoStr} • ${currentLabel}` : timeAgoStr;598599return {600label: i.title,601description: description,602chat: i,603buttons: i.isActive ? [renameButton] : [604renameButton,605openInEditorButton,606deleteButton,607]608};609});610611const fastPickItems = showAllChats ? allFastPickItems : allFastPickItems.slice(0, 5);612const fastPicks: (IQuickPickSeparator | IChatPickerItem)[] = [];613if (fastPickItems.length > 0) {614fastPicks.push({615type: 'separator',616label: localize('chat.history.recent', 'Recent Chats'),617});618fastPicks.push(...fastPickItems);619620// Add "Show more..." if there are more items and we're not showing all chats621if (!showAllChats && allFastPickItems.length > 5) {622fastPicks.push({623label: localize('chat.history.showMore', 'Show more...'),624description: '',625chat: {626sessionId: 'show-more-chats',627title: 'Show more...',628isActive: false,629lastMessageDate: 0,630},631buttons: []632});633}634}635636// Slow picks: Get coding agents asynchronously via AsyncIterable637const slowPicks = (async function* (): AsyncGenerator<(IQuickPickSeparator | ICodingAgentPickerItem)[]> {638try {639const agentPicks: ICodingAgentPickerItem[] = [];640641// Use the new Promise-based API to get chat sessions642const cancellationToken = new CancellationTokenSource();643try {644const providers = chatSessionsService.getAllChatSessionContributions();645const providerNSessions: { providerType: string; session: IChatSessionItem }[] = [];646647for (const provider of providers) {648const sessions = await chatSessionsService.provideChatSessionItems(provider.type, cancellationToken.token);649providerNSessions.push(...sessions.map(session => ({ providerType: provider.type, session })));650}651652for (const session of providerNSessions) {653const sessionContent = session.session;654655const ckey = contextKeyService.createKey('chatSessionType', session.providerType);656const actions = menuService.getMenuActions(MenuId.ChatSessionsMenu, contextKeyService);657const { primary } = getContextMenuActions(actions, 'inline');658ckey.reset();659660// Use primary actions if available, otherwise fall back to secondary actions661const buttons = primary.map(action => ({662id: action.id,663tooltip: action.tooltip,664iconClass: action.class || ThemeIcon.asClassName(Codicon.symbolClass),665}));666// Create agent pick from the session content667const agentPick: ICodingAgentPickerItem = {668label: sessionContent.label,669description: '',670session: { providerType: session.providerType, session: sessionContent },671chat: {672sessionId: sessionContent.id,673title: sessionContent.label,674isActive: false,675lastMessageDate: 0,676},677buttons,678id: sessionContent.id679};680681// Check if this agent already exists (update existing or add new)682const existingIndex = agentPicks.findIndex(pick => pick.chat.sessionId === sessionContent.id);683if (existingIndex >= 0) {684agentPicks[existingIndex] = agentPick;685} else {686// Respect show limits687const maxToShow = showAllAgents ? Number.MAX_SAFE_INTEGER : 5;688if (agentPicks.length < maxToShow) {689agentPicks.push(agentPick);690}691}692}693694// Create current picks with separator if we have agents695const currentPicks: (IQuickPickSeparator | ICodingAgentPickerItem)[] = [];696697if (agentPicks.length > 0) {698// Always add separator for coding agents section699currentPicks.push({700type: 'separator',701label: 'Chat Sessions',702});703currentPicks.push(...agentPicks);704705// Add "Show more..." if needed and not showing all agents706if (!showAllAgents && providerNSessions.length > 5) {707currentPicks.push({708label: localize('chat.history.showMoreAgents', 'Show more...'),709description: '',710chat: {711sessionId: 'show-more-agents',712title: 'Show more...',713isActive: false,714lastMessageDate: 0,715},716buttons: [],717uri: undefined,718});719}720}721722// Yield the current state723yield currentPicks;724725} finally {726cancellationToken.dispose();727}728729} catch (error) {730// Gracefully handle errors in async contributions731return;732}733})();734735// Return fast picks immediately, add slow picks as async generator736return {737fast: coalesce(fastPicks),738slow: slowPicks739};740};741742const store = new (DisposableStore as { new(): DisposableStore })();743const picker = store.add(quickInputService.createQuickPick<IChatPickerItem | ICodingAgentPickerItem>({ useSeparators: true }));744picker.title = (showAllChats || showAllAgents) ?745localize('interactiveSession.history.titleAll', "All Workspace Chat History") :746localize('interactiveSession.history.title', "Workspace Chat History");747picker.placeholder = localize('interactiveSession.history.pick', "Switch to chat");748picker.buttons = [clearChatHistoryButton];749750// Get fast and slow picks751const { fast, slow } = await getPicks(showAllChats, showAllAgents);752753// Set fast picks immediately754picker.items = fast;755picker.busy = true;756757// Consume slow picks progressively758(async () => {759try {760for await (const slowPicks of slow) {761if (!store.isDisposed) {762picker.items = coalesce([...fast, ...slowPicks]);763}764}765} catch (error) {766// Handle errors gracefully767} finally {768if (!store.isDisposed) {769picker.busy = false;770}771}772})();773store.add(picker.onDidTriggerButton(async button => {774if (button === clearChatHistoryButton) {775await commandService.executeCommand(CHAT_CLEAR_HISTORY_ACTION_ID);776}777}));778store.add(picker.onDidTriggerItemButton(async context => {779if (context.button === openInEditorButton) {780const options: IChatEditorOptions = { target: { sessionId: context.item.chat.sessionId }, pinned: true };781editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options }, ACTIVE_GROUP);782picker.hide();783} else if (context.button === deleteButton) {784chatService.removeHistoryEntry(context.item.chat.sessionId);785// Refresh picker items after deletion786const { fast, slow } = await getPicks(showAllChats, showAllAgents);787picker.items = fast;788picker.busy = true;789790// Consume slow picks progressively after deletion791(async () => {792try {793for await (const slowPicks of slow) {794if (!store.isDisposed) {795picker.items = coalesce([...fast, ...slowPicks]);796}797}798} catch (error) {799// Handle errors gracefully800} finally {801if (!store.isDisposed) {802picker.busy = false;803}804}805})();806} else if (context.button === renameButton) {807const title = await quickInputService.input({ title: localize('newChatTitle', "New chat title"), value: context.item.chat.title });808if (title) {809chatService.setChatSessionTitle(context.item.chat.sessionId, title);810}811812// The quick input hides the picker, it gets disposed, so we kick it off from scratch813await this.showIntegratedPicker(814chatService,815quickInputService,816commandService,817editorService,818chatWidgetService,819view,820chatSessionsService,821contextKeyService,822menuService,823showAllChats,824showAllAgents825);826} else {827const buttonItem = context.button as ICodingAgentPickerItem;828if (buttonItem.id) {829const contextItem = context.item as ICodingAgentPickerItem;830commandService.executeCommand(buttonItem.id, {831uri: contextItem.uri,832session: contextItem.session?.session,833$mid: MarshalledId.ChatSessionContext834});835836// dismiss quick picker837picker.hide();838}839}840}));841store.add(picker.onDidAccept(async () => {842try {843const item = picker.selectedItems[0];844const sessionId = item.chat.sessionId;845846// Handle "Show more..." options847if (sessionId === 'show-more-chats') {848picker.hide();849// Create a new picker with all chat items expanded850await this.showIntegratedPicker(851chatService,852quickInputService,853commandService,854editorService,855chatWidgetService,856view,857chatSessionsService,858contextKeyService,859menuService,860true,861showAllAgents862);863return;864} else if (sessionId === 'show-more-agents') {865picker.hide();866// Create a new picker with all agent items expanded867await this.showIntegratedPicker(868chatService,869quickInputService,870commandService,871editorService,872chatWidgetService,873view,874chatSessionsService,875contextKeyService,876menuService,877showAllChats,878true879);880return;881} else if ((item as ICodingAgentPickerItem).id !== undefined) {882// TODO: This is a temporary change that will be replaced by opening a new chat instance883const codingAgentItem = item as ICodingAgentPickerItem;884if (codingAgentItem.session) {885await this.showChatSessionInEditor(codingAgentItem.session.providerType, codingAgentItem.session.session, editorService);886}887}888889await view.loadSession(sessionId);890} finally {891picker.hide();892}893}));894store.add(picker.onDidHide(() => store.dispose()));895896picker.show();897};898899async run(accessor: ServicesAccessor) {900const chatService = accessor.get(IChatService);901const quickInputService = accessor.get(IQuickInputService);902const viewsService = accessor.get(IViewsService);903const editorService = accessor.get(IEditorService);904const chatWidgetService = accessor.get(IChatWidgetService);905const dialogService = accessor.get(IDialogService);906const commandService = accessor.get(ICommandService);907const chatSessionsService = accessor.get(IChatSessionsService);908const contextKeyService = accessor.get(IContextKeyService);909const menuService = accessor.get(IMenuService);910911const view = await viewsService.openView<ChatViewPane>(ChatViewId);912if (!view) {913return;914}915916const chatSessionId = view.widget.viewModel?.model.sessionId;917if (!chatSessionId) {918return;919}920921const editingSession = view.widget.viewModel?.model.editingSession;922if (editingSession) {923const phrase = localize('switchChat.confirmPhrase', "Switching chats will end your current edit session.");924if (!await handleCurrentEditingSession(editingSession, phrase, dialogService)) {925return;926}927}928929// Check if there are any non-local chat session item providers registered930const allProviders = chatSessionsService.getAllChatSessionItemProviders();931const hasNonLocalProviders = allProviders.some(provider => provider.chatSessionType !== 'local');932933if (hasNonLocalProviders) {934await this.showIntegratedPicker(935chatService,936quickInputService,937commandService,938editorService,939chatWidgetService,940view,941chatSessionsService,942contextKeyService,943menuService944);945} else {946await this.showLegacyPicker(chatService, quickInputService, commandService, editorService, view);947}948}949950private async showChatSessionInEditor(providerType: string, session: IChatSessionItem, editorService: IEditorService) {951// Open the chat editor952await editorService.openEditor({953resource: ChatSessionUri.forSession(providerType, session.id),954options: {} satisfies IChatEditorOptions955});956}957});958959registerAction2(class NewChatEditorAction extends Action2 {960constructor() {961super({962id: `workbench.action.openChat`,963title: localize2('interactiveSession.open', "New Chat Editor"),964f1: true,965category: CHAT_CATEGORY,966precondition: ChatContextKeys.enabled,967keybinding: {968weight: KeybindingWeight.WorkbenchContrib + 1,969primary: KeyMod.CtrlCmd | KeyCode.KeyN,970when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatEditor)971},972menu: {973id: MenuId.ChatTitleBarMenu,974group: 'b_new',975order: 0976}977});978}979980async run(accessor: ServicesAccessor) {981const editorService = accessor.get(IEditorService);982await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions });983}984});985986registerAction2(class NewChatWindowAction extends Action2 {987constructor() {988super({989id: `workbench.action.newChatWindow`,990title: localize2('interactiveSession.newChatWindow', "New Chat Window"),991f1: true,992category: CHAT_CATEGORY,993precondition: ChatContextKeys.enabled,994menu: {995id: MenuId.ChatTitleBarMenu,996group: 'b_new',997order: 1998}999});1000}10011002async run(accessor: ServicesAccessor) {1003const editorService = accessor.get(IEditorService);1004await editorService.openEditor({ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true, auxiliary: { compact: true, bounds: { width: 640, height: 640 } } } satisfies IChatEditorOptions }, AUX_WINDOW_GROUP);1005}1006});10071008registerAction2(class OpenChatEditorInNewWindowAction extends Action2 {1009constructor() {1010super({1011id: `workbench.action.chat.newChatInNewWindow`,1012title: localize2('chatSessions.openNewChatInNewWindow', 'Open New Chat in New Window'),1013f1: false,1014category: CHAT_CATEGORY,1015precondition: ChatContextKeys.enabled,1016menu: {1017id: MenuId.ViewTitle,1018group: 'submenu',1019order: 1,1020when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),1021}1022});1023}10241025async run(accessor: ServicesAccessor) {1026const editorService = accessor.get(IEditorService);1027await editorService.openEditor({1028resource: ChatEditorInput.getNewEditorUri(),1029options: {1030pinned: true,1031auxiliary: { compact: false }1032} satisfies IChatEditorOptions1033}, AUX_WINDOW_GROUP);1034}1035});10361037registerAction2(class NewChatInSideBarAction extends Action2 {1038constructor() {1039super({1040id: `workbench.action.chat.newChatInSideBar`,1041title: localize2('chatSessions.newChatInSideBar', 'Open New Chat in Side Bar'),1042f1: false,1043category: CHAT_CATEGORY,1044precondition: ChatContextKeys.enabled,1045menu: {1046id: MenuId.ViewTitle,1047group: 'submenu',1048order: 1,1049when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),1050}1051});1052}10531054async run(accessor: ServicesAccessor) {1055const viewsService = accessor.get(IViewsService);10561057// Open the chat view in the sidebar and get the widget1058const chatWidget = await showChatView(viewsService);10591060if (chatWidget) {1061// Clear the current chat to start a new one1062chatWidget.clear();1063await chatWidget.waitForReady();1064chatWidget.attachmentModel.clear(true);1065chatWidget.input.relatedFiles?.clear();10661067// Focus the input area1068chatWidget.focusInput();1069}1070}1071});10721073registerAction2(class OpenChatInNewEditorGroupAction extends Action2 {1074constructor() {1075super({1076id: 'workbench.action.chat.openNewChatToTheSide',1077title: localize2('chat.openNewChatToTheSide.label', "Open New Chat Editor to the Side"),1078category: CHAT_CATEGORY,1079precondition: ChatContextKeys.enabled,1080f1: false,1081menu: {1082id: MenuId.ViewTitle,1083group: 'submenu',1084order: 1,1085when: ContextKeyExpr.equals('view', `${VIEWLET_ID}.local`),1086}1087});1088}10891090async run(accessor: ServicesAccessor, ...args: any[]) {1091const editorService = accessor.get(IEditorService);1092const editorGroupService = accessor.get(IEditorGroupsService);10931094// Create a new editor group to the right1095const newGroup = editorGroupService.addGroup(editorGroupService.activeGroup, GroupDirection.RIGHT);1096editorGroupService.activateGroup(newGroup);10971098// Open a new chat editor in the new group1099await editorService.openEditor(1100{ resource: ChatEditorInput.getNewEditorUri(), options: { pinned: true } },1101newGroup.id1102);1103}1104});11051106registerAction2(class ChatAddAction extends Action2 {1107constructor() {1108super({1109id: 'workbench.action.chat.addParticipant',1110title: localize2('chatWith', "Chat with Extension"),1111icon: Codicon.mention,1112f1: false,1113category: CHAT_CATEGORY,1114menu: [{1115id: MenuId.ChatExecute,1116when: ContextKeyExpr.and(1117ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),1118ContextKeyExpr.not('config.chat.emptyChatState.enabled'),1119ChatContextKeys.lockedToCodingAgent.negate()1120),1121group: 'navigation',1122order: 11123}]1124});1125}11261127override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {1128const widgetService = accessor.get(IChatWidgetService);1129const context: { widget?: IChatWidget } | undefined = args[0];1130const widget = context?.widget ?? widgetService.lastFocusedWidget;1131if (!widget) {1132return;1133}11341135const hasAgentOrCommand = extractAgentAndCommand(widget.parsedInput);1136if (hasAgentOrCommand?.agentPart || hasAgentOrCommand?.commandPart) {1137return;1138}11391140const suggestCtrl = SuggestController.get(widget.inputEditor);1141if (suggestCtrl) {1142const curText = widget.inputEditor.getValue();1143const newValue = curText ? `@ ${curText}` : '@';1144if (!curText.startsWith('@')) {1145widget.inputEditor.setValue(newValue);1146}11471148widget.inputEditor.setPosition(new Position(1, 2));1149suggestCtrl.triggerSuggest(undefined, true);1150}1151}1152});11531154registerAction2(class ClearChatInputHistoryAction extends Action2 {1155constructor() {1156super({1157id: 'workbench.action.chat.clearInputHistory',1158title: localize2('interactiveSession.clearHistory.label', "Clear Input History"),1159precondition: ChatContextKeys.enabled,1160category: CHAT_CATEGORY,1161f1: true,1162});1163}1164async run(accessor: ServicesAccessor, ...args: any[]) {1165const historyService = accessor.get(IChatWidgetHistoryService);1166historyService.clearHistory();1167}1168});11691170registerAction2(class ClearChatHistoryAction extends Action2 {1171constructor() {1172super({1173id: CHAT_CLEAR_HISTORY_ACTION_ID,1174title: localize2('chat.clear.label', "Clear All Workspace Chats"),1175precondition: ChatContextKeys.enabled,1176category: CHAT_CATEGORY,1177f1: true,1178});1179}1180async run(accessor: ServicesAccessor, ...args: any[]) {1181const editorGroupsService = accessor.get(IEditorGroupsService);1182const chatService = accessor.get(IChatService);1183const instantiationService = accessor.get(IInstantiationService);1184const widgetService = accessor.get(IChatWidgetService);11851186await chatService.clearAllHistoryEntries();11871188widgetService.getAllWidgets().forEach(widget => {1189widget.clear();1190});11911192// Clear all chat editors. Have to go this route because the chat editor may be in the background and1193// not have a ChatEditorInput.1194editorGroupsService.groups.forEach(group => {1195group.editors.forEach(editor => {1196if (editor instanceof ChatEditorInput) {1197instantiationService.invokeFunction(clearChatEditor, editor);1198}1199});1200});1201}1202});12031204registerAction2(class FocusChatAction extends EditorAction2 {1205constructor() {1206super({1207id: 'chat.action.focus',1208title: localize2('actions.interactiveSession.focus', 'Focus Chat List'),1209precondition: ContextKeyExpr.and(ChatContextKeys.inChatInput),1210category: CHAT_CATEGORY,1211keybinding: [1212// On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top1213{1214when: ContextKeyExpr.and(ChatContextKeys.inputCursorAtTop, ChatContextKeys.inQuickChat.negate()),1215primary: KeyMod.CtrlCmd | KeyCode.UpArrow,1216weight: KeybindingWeight.EditorContrib,1217},1218// On win/linux, ctrl+up can always focus the chat list1219{1220when: ContextKeyExpr.and(ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), ChatContextKeys.inQuickChat.negate()),1221primary: KeyMod.CtrlCmd | KeyCode.UpArrow,1222weight: KeybindingWeight.EditorContrib,1223},1224{1225when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inQuickChat),1226primary: KeyMod.CtrlCmd | KeyCode.DownArrow,1227weight: KeybindingWeight.WorkbenchContrib,1228}1229]1230});1231}12321233runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {1234const editorUri = editor.getModel()?.uri;1235if (editorUri) {1236const widgetService = accessor.get(IChatWidgetService);1237widgetService.getWidgetByInputUri(editorUri)?.focusLastMessage();1238}1239}1240});12411242registerAction2(class FocusChatInputAction extends Action2 {1243constructor() {1244super({1245id: 'workbench.action.chat.focusInput',1246title: localize2('interactiveSession.focusInput.label', "Focus Chat Input"),1247f1: false,1248keybinding: [1249{1250primary: KeyMod.CtrlCmd | KeyCode.DownArrow,1251weight: KeybindingWeight.WorkbenchContrib,1252when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat.negate()),1253},1254{1255when: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.inChatInput.negate(), ChatContextKeys.inQuickChat),1256primary: KeyMod.CtrlCmd | KeyCode.UpArrow,1257weight: KeybindingWeight.WorkbenchContrib,1258}1259]1260});1261}1262run(accessor: ServicesAccessor, ...args: any[]) {1263const widgetService = accessor.get(IChatWidgetService);1264widgetService.lastFocusedWidget?.focusInput();1265}1266});12671268const nonEnterpriseCopilotUsers = ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.notEquals(`config.${defaultChat.completionsAdvancedSetting}.authProvider`, defaultChat.provider.enterprise.id));1269registerAction2(class extends Action2 {1270constructor() {1271super({1272id: 'workbench.action.chat.manageSettings',1273title: localize2('manageChat', "Manage Chat"),1274category: CHAT_CATEGORY,1275f1: true,1276precondition: ContextKeyExpr.and(1277ContextKeyExpr.or(1278ChatContextKeys.Entitlement.planFree,1279ChatContextKeys.Entitlement.planPro,1280ChatContextKeys.Entitlement.planProPlus1281),1282nonEnterpriseCopilotUsers1283),1284menu: {1285id: MenuId.ChatTitleBarMenu,1286group: 'y_manage',1287order: 1,1288when: nonEnterpriseCopilotUsers1289}1290});1291}12921293override async run(accessor: ServicesAccessor): Promise<void> {1294const openerService = accessor.get(IOpenerService);1295openerService.open(URI.parse(defaultChat.manageSettingsUrl));1296}1297});12981299registerAction2(class ShowExtensionsUsingCopilot extends Action2 {13001301constructor() {1302super({1303id: 'workbench.action.chat.showExtensionsUsingCopilot',1304title: localize2('showCopilotUsageExtensions', "Show Extensions using Copilot"),1305f1: true,1306category: EXTENSIONS_CATEGORY,1307precondition: ChatContextKeys.enabled1308});1309}13101311override async run(accessor: ServicesAccessor): Promise<void> {1312const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);1313extensionsWorkbenchService.openSearch(`@feature:${CopilotUsageExtensionFeatureId}`);1314}1315});13161317registerAction2(class ConfigureCopilotCompletions extends Action2 {13181319constructor() {1320super({1321id: 'workbench.action.chat.configureCodeCompletions',1322title: localize2('configureCompletions', "Configure Code Completions..."),1323precondition: ContextKeyExpr.and(1324ChatContextKeys.Setup.installed,1325ChatContextKeys.Setup.disabled.negate(),1326ChatContextKeys.Setup.untrusted.negate()1327),1328menu: {1329id: MenuId.ChatTitleBarMenu,1330group: 'f_completions',1331order: 10,1332}1333});1334}13351336override async run(accessor: ServicesAccessor): Promise<void> {1337const commandService = accessor.get(ICommandService);1338commandService.executeCommand(defaultChat.completionsMenuCommand);1339}1340});13411342registerAction2(class ShowQuotaExceededDialogAction extends Action2 {13431344constructor() {1345super({1346id: OPEN_CHAT_QUOTA_EXCEEDED_DIALOG,1347title: localize('upgradeChat', "Upgrade GitHub Copilot Plan")1348});1349}13501351override async run(accessor: ServicesAccessor) {1352const chatEntitlementService = accessor.get(IChatEntitlementService);1353const commandService = accessor.get(ICommandService);1354const dialogService = accessor.get(IDialogService);1355const telemetryService = accessor.get(ITelemetryService);13561357let message: string;1358const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0;1359const completionsQuotaExceeded = chatEntitlementService.quotas.completions?.percentRemaining === 0;1360if (chatQuotaExceeded && !completionsQuotaExceeded) {1361message = localize('chatQuotaExceeded', "You've reached your monthly chat messages quota. You still have free code completions available.");1362} else if (completionsQuotaExceeded && !chatQuotaExceeded) {1363message = localize('completionsQuotaExceeded', "You've reached your monthly code completions quota. You still have free chat messages available.");1364} else {1365message = localize('chatAndCompletionsQuotaExceeded', "You've reached your monthly chat messages and code completions quota.");1366}13671368if (chatEntitlementService.quotas.resetDate) {1369const 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' });1370const quotaResetDate = new Date(chatEntitlementService.quotas.resetDate);1371message = [message, localize('quotaResetDate', "The allowance will reset on {0}.", dateFormatter.value.format(quotaResetDate))].join(' ');1372}13731374const free = chatEntitlementService.entitlement === ChatEntitlement.Free;1375const upgradeToPro = free ? localize('upgradeToPro', "Upgrade to GitHub Copilot Pro (your first 30 days are free) for:\n- Unlimited code completions\n- Unlimited chat messages\n- Access to premium models") : undefined;13761377await dialogService.prompt({1378type: 'none',1379message: localize('copilotQuotaReached', "GitHub Copilot Quota Reached"),1380cancelButton: {1381label: localize('dismiss', "Dismiss"),1382run: () => { /* noop */ }1383},1384buttons: [1385{1386label: free ? localize('upgradePro', "Upgrade to GitHub Copilot Pro") : localize('upgradePlan', "Upgrade GitHub Copilot Plan"),1387run: () => {1388const commandId = 'workbench.action.chat.upgradePlan';1389telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: commandId, from: 'chat-dialog' });1390commandService.executeCommand(commandId);1391}1392},1393],1394custom: {1395icon: Codicon.copilotWarningLarge,1396markdownDetails: coalesce([1397{ markdown: new MarkdownString(message, true) },1398upgradeToPro ? { markdown: new MarkdownString(upgradeToPro, true) } : undefined1399])1400}1401});1402}1403});14041405registerAction2(class ResetTrustedToolsAction extends Action2 {1406constructor() {1407super({1408id: 'workbench.action.chat.resetTrustedTools',1409title: localize2('resetTrustedTools', "Reset Tool Confirmations"),1410category: CHAT_CATEGORY,1411f1: true,1412precondition: ChatContextKeys.enabled1413});1414}1415override run(accessor: ServicesAccessor): void {1416accessor.get(ILanguageModelToolsService).resetToolAutoConfirmation();1417accessor.get(INotificationService).info(localize('resetTrustedToolsSuccess', "Tool confirmation preferences have been reset."));1418}1419});14201421registerAction2(class UpdateInstructionsAction extends Action2 {1422constructor() {1423super({1424id: 'workbench.action.chat.generateInstructions',1425title: localize2('generateInstructions', "Generate Workspace Instructions File"),1426shortTitle: localize2('generateInstructions.short', "Generate Instructions"),1427category: CHAT_CATEGORY,1428icon: Codicon.sparkle,1429f1: true,1430precondition: ChatContextKeys.enabled,1431menu: {1432id: CHAT_CONFIG_MENU_ID,1433when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),1434order: 13,1435group: '1_level'1436}1437});1438}14391440async run(accessor: ServicesAccessor): Promise<void> {1441const commandService = accessor.get(ICommandService);14421443// Use chat command to open and send the query1444const query = `Analyze this codebase to generate or update \`.github/copilot-instructions.md\` for guiding AI coding agents.14451446Focus on discovering the essential knowledge that would help an AI agents be immediately productive in this codebase. Consider aspects like:1447- The "big picture" architecture that requires reading multiple files to understand - major components, service boundaries, data flows, and the "why" behind structural decisions1448- Critical developer workflows (builds, tests, debugging) especially commands that aren't obvious from file inspection alone1449- Project-specific conventions and patterns that differ from common practices1450- Integration points, external dependencies, and cross-component communication patterns14511452Source 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).14531454Guidelines (read more at https://aka.ms/vscode-instructions-docs):1455- If \`.github/copilot-instructions.md\` exists, merge intelligently - preserve valuable content while updating outdated sections1456- Write concise, actionable instructions (~20-50 lines) using markdown structure1457- Include specific examples from the codebase when describing patterns1458- Avoid generic advice ("write tests", "handle errors") - focus on THIS project's specific approaches1459- Document only discoverable patterns, not aspirational practices1460- Reference key files/directories that exemplify important patterns14611462Update \`.github/copilot-instructions.md\` for the user, then ask for feedback on any unclear or incomplete sections to iterate.`;14631464await commandService.executeCommand('workbench.action.chat.open', {1465mode: 'agent',1466query: query,1467});1468}1469});14701471registerAction2(class OpenChatFeatureSettingsAction extends Action2 {1472constructor() {1473super({1474id: 'workbench.action.chat.openFeatureSettings',1475title: localize2('openChatFeatureSettings', "Chat Settings"),1476shortTitle: localize('openChatFeatureSettings.short', "Chat Settings"),1477category: CHAT_CATEGORY,1478f1: true,1479precondition: ChatContextKeys.enabled,1480menu: {1481id: CHAT_CONFIG_MENU_ID,1482when: ContextKeyExpr.and(ChatContextKeys.enabled, ContextKeyExpr.equals('view', ChatViewId)),1483order: 15,1484group: '2_configure'1485}1486});1487}14881489override async run(accessor: ServicesAccessor): Promise<void> {1490const preferencesService = accessor.get(IPreferencesService);1491preferencesService.openSettings({ query: '@feature:chat' });1492}1493});14941495MenuRegistry.appendMenuItem(MenuId.ViewTitle, {1496submenu: CHAT_CONFIG_MENU_ID,1497title: localize2('config.label', "Configure Chat..."),1498group: 'navigation',1499when: ContextKeyExpr.equals('view', ChatViewId),1500icon: Codicon.settingsGear,1501order: 61502});1503}15041505export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string {1506if (isRequestVM(item)) {1507return (includeName ? `${item.username}: ` : '') + item.messageText;1508} else {1509return (includeName ? `${item.username}: ` : '') + item.response.toString();1510}1511}151215131514// --- Title Bar Chat Controls15151516const defaultChat = {1517documentationUrl: product.defaultChatAgent?.documentationUrl ?? '',1518manageSettingsUrl: product.defaultChatAgent?.manageSettingsUrl ?? '',1519managePlanUrl: product.defaultChatAgent?.managePlanUrl ?? '',1520provider: product.defaultChatAgent?.provider ?? { enterprise: { id: '' } },1521completionsAdvancedSetting: product.defaultChatAgent?.completionsAdvancedSetting ?? '',1522completionsMenuCommand: product.defaultChatAgent?.completionsMenuCommand ?? '',1523};15241525// Add next to the command center if command center is disabled1526MenuRegistry.appendMenuItem(MenuId.CommandCenter, {1527submenu: MenuId.ChatTitleBarMenu,1528title: localize('title4', "Chat"),1529icon: Codicon.chatSparkle,1530when: ContextKeyExpr.and(1531ChatContextKeys.supported,1532ContextKeyExpr.and(1533ChatContextKeys.Setup.hidden.negate(),1534ChatContextKeys.Setup.disabled.negate()1535),1536ContextKeyExpr.has('config.chat.commandCenter.enabled')1537),1538order: 10001 // to the right of command center1539});15401541// Add to the global title bar if command center is disabled1542MenuRegistry.appendMenuItem(MenuId.TitleBar, {1543submenu: MenuId.ChatTitleBarMenu,1544title: localize('title4', "Chat"),1545group: 'navigation',1546icon: Codicon.chatSparkle,1547when: ContextKeyExpr.and(1548ChatContextKeys.supported,1549ContextKeyExpr.and(1550ChatContextKeys.Setup.hidden.negate(),1551ChatContextKeys.Setup.disabled.negate()1552),1553ContextKeyExpr.has('config.chat.commandCenter.enabled'),1554ContextKeyExpr.has('config.window.commandCenter').negate(),1555),1556order: 11557});15581559registerAction2(class ToggleCopilotControl extends ToggleTitleBarConfigAction {1560constructor() {1561super(1562'chat.commandCenter.enabled',1563localize('toggle.chatControl', 'Chat Controls'),1564localize('toggle.chatControlsDescription', "Toggle visibility of the Chat Controls in title bar"), 5,1565ContextKeyExpr.and(1566ContextKeyExpr.and(1567ChatContextKeys.Setup.hidden.negate(),1568ChatContextKeys.Setup.disabled.negate()1569),1570IsCompactTitleBarContext.negate(),1571ChatContextKeys.supported1572)1573);1574}1575});15761577export class CopilotTitleBarMenuRendering extends Disposable implements IWorkbenchContribution {15781579static readonly ID = 'workbench.contrib.copilotTitleBarMenuRendering';15801581constructor(1582@IActionViewItemService actionViewItemService: IActionViewItemService,1583@IChatEntitlementService chatEntitlementService: IChatEntitlementService,1584) {1585super();15861587const disposable = actionViewItemService.register(MenuId.CommandCenter, MenuId.ChatTitleBarMenu, (action, options, instantiationService, windowId) => {1588if (!(action instanceof SubmenuItemAction)) {1589return undefined;1590}15911592const dropdownAction = toAction({1593id: 'copilot.titleBarMenuRendering.more',1594label: localize('more', "More..."),1595run() { }1596});15971598const chatSentiment = chatEntitlementService.sentiment;1599const chatQuotaExceeded = chatEntitlementService.quotas.chat?.percentRemaining === 0;1600const signedOut = chatEntitlementService.entitlement === ChatEntitlement.Unknown;1601const free = chatEntitlementService.entitlement === ChatEntitlement.Free;16021603const isAuxiliaryWindow = windowId !== mainWindow.vscodeWindowId;1604let primaryActionId = isAuxiliaryWindow ? CHAT_OPEN_ACTION_ID : TOGGLE_CHAT_ACTION_ID;1605let primaryActionTitle = isAuxiliaryWindow ? localize('openChat', "Open Chat") : localize('toggleChat', "Toggle Chat");1606let primaryActionIcon = Codicon.chatSparkle;1607if (chatSentiment.installed && !chatSentiment.disabled) {1608if (signedOut) {1609primaryActionId = CHAT_SETUP_ACTION_ID;1610primaryActionTitle = localize('signInToChatSetup', "Sign in to use AI features...");1611primaryActionIcon = Codicon.chatSparkleError;1612} else if (chatQuotaExceeded && free) {1613primaryActionId = OPEN_CHAT_QUOTA_EXCEEDED_DIALOG;1614primaryActionTitle = localize('chatQuotaExceededButton', "GitHub Copilot Free plan chat messages quota reached. Click for details.");1615primaryActionIcon = Codicon.chatSparkleWarning;1616}1617}1618return instantiationService.createInstance(DropdownWithPrimaryActionViewItem, instantiationService.createInstance(MenuItemAction, {1619id: primaryActionId,1620title: primaryActionTitle,1621icon: primaryActionIcon,1622}, undefined, undefined, undefined, undefined), dropdownAction, action.actions, '', { ...options, skipTelemetry: true });1623}, Event.any(1624chatEntitlementService.onDidChangeSentiment,1625chatEntitlementService.onDidChangeQuotaExceeded,1626chatEntitlementService.onDidChangeEntitlement1627));16281629// Reduces flicker a bit on reload/restart1630markAsSingleton(disposable);1631}1632}16331634/**1635* Returns whether we can continue clearing/switching chat sessions, false to cancel.1636*/1637export async function handleCurrentEditingSession(currentEditingSession: IChatEditingSession, phrase: string | undefined, dialogService: IDialogService): Promise<boolean> {1638if (shouldShowClearEditingSessionConfirmation(currentEditingSession)) {1639return showClearEditingSessionConfirmation(currentEditingSession, dialogService, { messageOverride: phrase });1640}16411642return true;1643}16441645/**1646* Returns whether we can switch the chat mode, based on whether the user had to agree to clear the session, false to cancel.1647*/1648export async function handleModeSwitch(1649accessor: ServicesAccessor,1650fromMode: ChatModeKind,1651toMode: ChatModeKind,1652requestCount: number,1653editingSession: IChatEditingSession | undefined,1654): Promise<false | { needToClearSession: boolean }> {1655if (!editingSession || fromMode === toMode) {1656return { needToClearSession: false };1657}16581659const configurationService = accessor.get(IConfigurationService);1660const dialogService = accessor.get(IDialogService);1661const needToClearEdits = (!configurationService.getValue(ChatConfiguration.Edits2Enabled) && (fromMode === ChatModeKind.Edit || toMode === ChatModeKind.Edit)) && requestCount > 0;1662if (needToClearEdits) {1663// If not using edits2 and switching into or out of edit mode, ask to discard the session1664const phrase = localize('switchMode.confirmPhrase', "Switching chat modes will end your current edit session.");16651666const currentEdits = editingSession.entries.get();1667const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified);1668if (undecidedEdits.length > 0) {1669if (!await handleCurrentEditingSession(editingSession, phrase, dialogService)) {1670return false;1671}16721673return { needToClearSession: true };1674} else {1675const confirmation = await dialogService.confirm({1676title: localize('agent.newSession', "Start new session?"),1677message: localize('agent.newSessionMessage', "Changing the chat mode will end your current edit session. Would you like to change the chat mode?"),1678primaryButton: localize('agent.newSession.confirm', "Yes"),1679type: 'info'1680});1681if (!confirmation.confirmed) {1682return false;1683}16841685return { needToClearSession: true };1686}1687}16881689return { needToClearSession: false };1690}16911692export interface IClearEditingSessionConfirmationOptions {1693titleOverride?: string;1694messageOverride?: string;1695}169616971698// --- Chat Submenus in various Components16991700MenuRegistry.appendMenuItem(MenuId.EditorContext, {1701submenu: MenuId.ChatTextEditorMenu,1702group: '1_chat',1703order: 5,1704title: localize('generateCode', "Generate Code"),1705when: ContextKeyExpr.and(1706ChatContextKeys.Setup.hidden.negate(),1707ChatContextKeys.Setup.disabled.negate()1708)1709});17101711// TODO@bpasero remove these when Chat extension is built-in1712{1713function registerGenerateCodeCommand(coreCommand: string, actualCommand: string): void {1714CommandsRegistry.registerCommand(coreCommand, async accessor => {1715const commandService = accessor.get(ICommandService);1716const telemetryService = accessor.get(ITelemetryService);1717const editorGroupService = accessor.get(IEditorGroupsService);17181719telemetryService.publicLog2<WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification>('workbenchActionExecuted', { id: CHAT_SETUP_ACTION_ID, from: 'editor' });17201721if (editorGroupService.activeGroup.activeEditor) {1722// Pinning the editor helps when the Chat extension welcome kicks in after install to keep context1723editorGroupService.activeGroup.pinEditor(editorGroupService.activeGroup.activeEditor);1724}17251726const result = await commandService.executeCommand(CHAT_SETUP_ACTION_ID);1727if (!result) {1728return;1729}17301731await commandService.executeCommand(actualCommand);1732});1733}1734registerGenerateCodeCommand('chat.internal.explain', 'github.copilot.chat.explain');1735registerGenerateCodeCommand('chat.internal.fix', 'github.copilot.chat.fix');1736registerGenerateCodeCommand('chat.internal.review', 'github.copilot.chat.review');1737registerGenerateCodeCommand('chat.internal.generateDocs', 'github.copilot.chat.generateDocs');1738registerGenerateCodeCommand('chat.internal.generateTests', 'github.copilot.chat.generateTests');17391740const internalGenerateCodeContext = ContextKeyExpr.and(1741ChatContextKeys.Setup.hidden.negate(),1742ChatContextKeys.Setup.disabled.negate(),1743ChatContextKeys.Setup.installed.negate(),1744);17451746MenuRegistry.appendMenuItem(MenuId.EditorContext, {1747command: {1748id: 'chat.internal.explain',1749title: localize('explain', "Explain"),1750},1751group: '1_chat',1752order: 4,1753when: internalGenerateCodeContext1754});17551756MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {1757command: {1758id: 'chat.internal.fix',1759title: localize('fix', "Fix"),1760},1761group: '1_action',1762order: 1,1763when: ContextKeyExpr.and(1764internalGenerateCodeContext,1765EditorContextKeys.readOnly.negate()1766)1767});17681769MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {1770command: {1771id: 'chat.internal.review',1772title: localize('review', "Code Review"),1773},1774group: '1_action',1775order: 2,1776when: internalGenerateCodeContext1777});17781779MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {1780command: {1781id: 'chat.internal.generateDocs',1782title: localize('generateDocs', "Generate Docs"),1783},1784group: '2_generate',1785order: 1,1786when: ContextKeyExpr.and(1787internalGenerateCodeContext,1788EditorContextKeys.readOnly.negate()1789)1790});17911792MenuRegistry.appendMenuItem(MenuId.ChatTextEditorMenu, {1793command: {1794id: 'chat.internal.generateTests',1795title: localize('generateTests', "Generate Tests"),1796},1797group: '2_generate',1798order: 2,1799when: ContextKeyExpr.and(1800internalGenerateCodeContext,1801EditorContextKeys.readOnly.negate()1802)1803});1804}180518061807// --- Chat Default Visibility18081809registerAction2(class ToggleDefaultVisibilityAction extends Action2 {1810constructor() {1811super({1812id: 'workbench.action.chat.toggleDefaultVisibility',1813title: localize2('chat.toggleDefaultVisibility.label', "Show View by Default"),1814toggled: ContextKeyExpr.equals('config.workbench.secondarySideBar.defaultVisibility', 'hidden').negate(),1815f1: false,1816menu: {1817id: MenuId.ViewTitle,1818when: ContextKeyExpr.and(1819ContextKeyExpr.equals('view', ChatViewId),1820ChatContextKeys.panelLocation.isEqualTo(ViewContainerLocation.AuxiliaryBar),1821),1822order: 0,1823group: '5_configure'1824},1825});1826}18271828async run(accessor: ServicesAccessor) {1829const configurationService = accessor.get(IConfigurationService);18301831const currentValue = configurationService.getValue<'hidden' | unknown>('workbench.secondarySideBar.defaultVisibility');1832configurationService.updateValue('workbench.secondarySideBar.defaultVisibility', currentValue !== 'hidden' ? 'hidden' : 'visible');1833}1834});18351836registerAction2(class EditToolApproval extends Action2 {1837constructor() {1838super({1839id: 'workbench.action.chat.editToolApproval',1840title: localize2('chat.editToolApproval.label', "Edit Tool Approval"),1841f1: false1842});1843}18441845async run(accessor: ServicesAccessor, toolId: string): Promise<void> {1846if (!toolId) {1847return;1848}18491850const quickInputService = accessor.get(IQuickInputService);1851const toolsService = accessor.get(ILanguageModelToolsService);1852const tool = toolsService.getTool(toolId);1853if (!tool) {1854return;1855}18561857const currentState = toolsService.getToolAutoConfirmation(toolId);18581859interface TItem extends IQuickPickItem {1860id: 'session' | 'workspace' | 'profile' | 'never';1861}18621863const items: TItem[] = [1864{ id: 'never', label: localize('chat.toolApproval.manual', "Always require manual approval") },1865{ id: 'session', label: localize('chat.toolApproval.session', "Auto-approve for this session") },1866{ id: 'workspace', label: localize('chat.toolApproval.workspace', "Auto-approve for this workspace") },1867{ id: 'profile', label: localize('chat.toolApproval.profile', "Auto-approve globally") }1868];18691870const quickPick = quickInputService.createQuickPick<TItem>();1871quickPick.placeholder = localize('chat.editToolApproval.title', "Approval setting for {0}", tool.displayName ?? tool.id);1872quickPick.items = items;1873quickPick.canSelectMany = false;1874quickPick.activeItems = items.filter(item => item.id === currentState);18751876const selection = await new Promise<TItem | undefined>((resolve) => {1877quickPick.onDidAccept(() => {1878const selected = quickPick.selectedItems[0];1879resolve(selected);1880});1881quickPick.onDidHide(() => {1882resolve(undefined);18831884});1885quickPick.show();1886});18871888quickPick.dispose();18891890if (selection) {1891toolsService.setToolAutoConfirmation(toolId, selection.id);1892}1893}1894});189518961897