Path: blob/main/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts
4780 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 { sep } from '../../../../../base/common/path.js';6import { raceCancellationError } from '../../../../../base/common/async.js';7import { CancellationToken } from '../../../../../base/common/cancellation.js';8import { Codicon } from '../../../../../base/common/codicons.js';9import { Emitter, Event } from '../../../../../base/common/event.js';10import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';11import { ResourceMap } from '../../../../../base/common/map.js';12import { Schemas } from '../../../../../base/common/network.js';13import * as resources from '../../../../../base/common/resources.js';14import { ThemeIcon } from '../../../../../base/common/themables.js';15import { URI, UriComponents } from '../../../../../base/common/uri.js';16import { generateUuid } from '../../../../../base/common/uuid.js';17import { localize, localize2 } from '../../../../../nls.js';18import { Action2, IMenuService, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';19import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';20import { IRelaxedExtensionDescription } from '../../../../../platform/extensions/common/extensions.js';21import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';22import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';23import { ILabelService } from '../../../../../platform/label/common/label.js';24import { ILogService } from '../../../../../platform/log/common/log.js';25import { isDark } from '../../../../../platform/theme/common/theme.js';26import { IThemeService } from '../../../../../platform/theme/common/themeService.js';27import { IEditorService } from '../../../../services/editor/common/editorService.js';28import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js';29import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js';30import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';31import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';32import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';33import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js';34import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';35import { CHAT_CATEGORY } from '../actions/chatActions.js';36import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';37import { IChatModel } from '../../common/model/chatModel.js';38import { IChatService, IChatToolInvocation } from '../../common/chatService/chatService.js';39import { autorun, autorunIterableDelta, observableSignalFromEvent } from '../../../../../base/common/observable.js';40import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js';41import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';42import { IMarkdownString } from '../../../../../base/common/htmlContent.js';43import { IViewsService } from '../../../../services/views/common/viewsService.js';44import { ChatViewId } from '../chat.js';45import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';46import { AgentSessionProviders } from '../agentSessions/agentSessions.js';4748const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsExtensionPoint[]>({49extensionPoint: 'chatSessions',50jsonSchema: {51description: localize('chatSessionsExtPoint', 'Contributes chat session integrations to the chat widget.'),52type: 'array',53items: {54type: 'object',55additionalProperties: false,56properties: {57type: {58description: localize('chatSessionsExtPoint.chatSessionType', 'Unique identifier for the type of chat session.'),59type: 'string',60},61name: {62description: localize('chatSessionsExtPoint.name', 'Name of the dynamically registered chat participant (eg: @agent). Must not contain whitespace.'),63type: 'string',64pattern: '^[\\w-]+$'65},66displayName: {67description: localize('chatSessionsExtPoint.displayName', 'A longer name for this item which is used for display in menus.'),68type: 'string',69},70description: {71description: localize('chatSessionsExtPoint.description', 'Description of the chat session for use in menus and tooltips.'),72type: 'string'73},74when: {75description: localize('chatSessionsExtPoint.when', 'Condition which must be true to show this item.'),76type: 'string'77},78icon: {79description: localize('chatSessionsExtPoint.icon', 'Icon identifier (codicon ID) for the chat session editor tab. For example, "$(github)" or "$(cloud)".'),80anyOf: [{81type: 'string'82},83{84type: 'object',85properties: {86light: {87description: localize('icon.light', 'Icon path when a light theme is used'),88type: 'string'89},90dark: {91description: localize('icon.dark', 'Icon path when a dark theme is used'),92type: 'string'93}94}95}]96},97order: {98description: localize('chatSessionsExtPoint.order', 'Order in which this item should be displayed.'),99type: 'integer'100},101alternativeIds: {102description: localize('chatSessionsExtPoint.alternativeIds', 'Alternative identifiers for backward compatibility.'),103type: 'array',104items: {105type: 'string'106}107},108welcomeTitle: {109description: localize('chatSessionsExtPoint.welcomeTitle', 'Title text to display in the chat welcome view for this session type.'),110type: 'string'111},112welcomeMessage: {113description: localize('chatSessionsExtPoint.welcomeMessage', 'Message text (supports markdown) to display in the chat welcome view for this session type.'),114type: 'string'115},116welcomeTips: {117description: localize('chatSessionsExtPoint.welcomeTips', 'Tips text (supports markdown and theme icons) to display in the chat welcome view for this session type.'),118type: 'string'119},120inputPlaceholder: {121description: localize('chatSessionsExtPoint.inputPlaceholder', 'Placeholder text to display in the chat input box for this session type.'),122type: 'string'123},124capabilities: {125description: localize('chatSessionsExtPoint.capabilities', 'Optional capabilities for this chat session.'),126type: 'object',127additionalProperties: false,128properties: {129supportsFileAttachments: {130description: localize('chatSessionsExtPoint.supportsFileAttachments', 'Whether this chat session supports attaching files or file references.'),131type: 'boolean'132},133supportsToolAttachments: {134description: localize('chatSessionsExtPoint.supportsToolAttachments', 'Whether this chat session supports attaching tools or tool references.'),135type: 'boolean'136},137supportsMCPAttachments: {138description: localize('chatSessionsExtPoint.supportsMCPAttachments', 'Whether this chat session supports attaching MCP resources.'),139type: 'boolean'140},141supportsImageAttachments: {142description: localize('chatSessionsExtPoint.supportsImageAttachments', 'Whether this chat session supports attaching images.'),143type: 'boolean'144},145supportsSearchResultAttachments: {146description: localize('chatSessionsExtPoint.supportsSearchResultAttachments', 'Whether this chat session supports attaching search results.'),147type: 'boolean'148},149supportsInstructionAttachments: {150description: localize('chatSessionsExtPoint.supportsInstructionAttachments', 'Whether this chat session supports attaching instructions.'),151type: 'boolean'152},153supportsSourceControlAttachments: {154description: localize('chatSessionsExtPoint.supportsSourceControlAttachments', 'Whether this chat session supports attaching source control changes.'),155type: 'boolean'156},157supportsProblemAttachments: {158description: localize('chatSessionsExtPoint.supportsProblemAttachments', 'Whether this chat session supports attaching problems.'),159type: 'boolean'160},161supportsSymbolAttachments: {162description: localize('chatSessionsExtPoint.supportsSymbolAttachments', 'Whether this chat session supports attaching symbols.'),163type: 'boolean'164}165}166},167commands: {168markdownDescription: localize('chatCommandsDescription', "Commands available for this chat session, which the user can invoke with a `/`."),169type: 'array',170items: {171additionalProperties: false,172type: 'object',173defaultSnippets: [{ body: { name: '', description: '' } }],174required: ['name'],175properties: {176name: {177description: localize('chatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."),178type: 'string'179},180description: {181description: localize('chatCommandDescription', "A description of this command."),182type: 'string'183},184when: {185description: localize('chatCommandWhen', "A condition which must be true to enable this command."),186type: 'string'187},188}189}190},191canDelegate: {192description: localize('chatSessionsExtPoint.canDelegate', 'Whether delegation is supported. Default is false. Note that enabling this is experimental and may not be respected at all times.'),193type: 'boolean',194default: false195}196},197required: ['type', 'name', 'displayName', 'description'],198}199},200activationEventsGenerator: function* (contribs) {201for (const contrib of contribs) {202yield `onChatSession:${contrib.type}`;203}204}205});206207class ContributedChatSessionData extends Disposable {208209private readonly _optionsCache: Map<string /* 'models' */, string | IChatSessionProviderOptionItem>;210public getOption(optionId: string): string | IChatSessionProviderOptionItem | undefined {211return this._optionsCache.get(optionId);212}213public setOption(optionId: string, value: string | IChatSessionProviderOptionItem): void {214this._optionsCache.set(optionId, value);215}216217constructor(218readonly session: IChatSession,219readonly chatSessionType: string,220readonly resource: URI,221readonly options: Record<string, string | IChatSessionProviderOptionItem> | undefined,222private readonly onWillDispose: (resource: URI) => void223) {224super();225226this._optionsCache = new Map<string, string | IChatSessionProviderOptionItem>();227if (options) {228for (const [key, value] of Object.entries(options)) {229this._optionsCache.set(key, value);230}231}232233this._register(this.session.onWillDispose(() => {234this.onWillDispose(this.resource);235}));236}237}238239240export class ChatSessionsService extends Disposable implements IChatSessionsService {241readonly _serviceBrand: undefined;242243private readonly _itemsProviders: Map</* type */ string, IChatSessionItemProvider> = new Map();244245private readonly _contributions: Map</* type */ string, { readonly contribution: IChatSessionsExtensionPoint; readonly extension: IRelaxedExtensionDescription }> = new Map();246private readonly _contributionDisposables = this._register(new DisposableMap</* type */ string>());247248private readonly _contentProviders: Map</* scheme */ string, IChatSessionContentProvider> = new Map();249private readonly _alternativeIdMap: Map</* alternativeId */ string, /* primaryType */ string> = new Map();250private readonly _contextKeys = new Set<string>();251252private readonly _onDidChangeItemsProviders = this._register(new Emitter<IChatSessionItemProvider>());253readonly onDidChangeItemsProviders: Event<IChatSessionItemProvider> = this._onDidChangeItemsProviders.event;254255private readonly _onDidChangeSessionItems = this._register(new Emitter<string>());256readonly onDidChangeSessionItems: Event<string> = this._onDidChangeSessionItems.event;257258private readonly _onDidChangeAvailability = this._register(new Emitter<void>());259readonly onDidChangeAvailability: Event<void> = this._onDidChangeAvailability.event;260261private readonly _onDidChangeInProgress = this._register(new Emitter<void>());262public get onDidChangeInProgress() { return this._onDidChangeInProgress.event; }263264private readonly _onDidChangeContentProviderSchemes = this._register(new Emitter<{ readonly added: string[]; readonly removed: string[] }>());265public get onDidChangeContentProviderSchemes() { return this._onDidChangeContentProviderSchemes.event; }266private readonly _onDidChangeSessionOptions = this._register(new Emitter<URI>());267public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; }268private readonly _onDidChangeOptionGroups = this._register(new Emitter<string>());269public get onDidChangeOptionGroups() { return this._onDidChangeOptionGroups.event; }270271private readonly inProgressMap: Map<string, number> = new Map();272private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();273private readonly _sessionTypeIcons: Map<string, ThemeIcon | { light: URI; dark: URI }> = new Map();274private readonly _sessionTypeWelcomeTitles: Map<string, string> = new Map();275private readonly _sessionTypeWelcomeMessages: Map<string, string> = new Map();276private readonly _sessionTypeWelcomeTips: Map<string, string> = new Map();277private readonly _sessionTypeInputPlaceholders: Map<string, string> = new Map();278279private readonly _sessions = new ResourceMap<ContributedChatSessionData>();280281private readonly _hasCanDelegateProvidersKey: IContextKey<boolean>;282283constructor(284@ILogService private readonly _logService: ILogService,285@IChatAgentService private readonly _chatAgentService: IChatAgentService,286@IExtensionService private readonly _extensionService: IExtensionService,287@IContextKeyService private readonly _contextKeyService: IContextKeyService,288@IMenuService private readonly _menuService: IMenuService,289@IThemeService private readonly _themeService: IThemeService,290@ILabelService private readonly _labelService: ILabelService291) {292super();293294this._hasCanDelegateProvidersKey = ChatContextKeys.hasCanDelegateProviders.bindTo(this._contextKeyService);295296this._register(extensionPoint.setHandler(extensions => {297for (const ext of extensions) {298if (!isProposedApiEnabled(ext.description, 'chatSessionsProvider')) {299continue;300}301if (!Array.isArray(ext.value)) {302continue;303}304for (const contribution of ext.value) {305this._register(this.registerContribution(contribution, ext.description));306}307}308}));309310// Listen for context changes and re-evaluate contributions311this._register(Event.filter(this._contextKeyService.onDidChangeContext, e => e.affectsSome(this._contextKeys))(() => {312this._evaluateAvailability();313}));314315this._register(this.onDidChangeSessionItems(chatSessionType => {316this.updateInProgressStatus(chatSessionType).catch(error => {317this._logService.warn(`Failed to update progress status for '${chatSessionType}':`, error);318});319}));320321this._register(this._labelService.registerFormatter({322scheme: Schemas.copilotPr,323formatting: {324label: '${authority}${path}',325separator: sep,326stripPathStartingSeparator: true,327}328}));329}330331public reportInProgress(chatSessionType: string, count: number): void {332let displayName: string | undefined;333334if (chatSessionType === AgentSessionProviders.Local) {335displayName = localize('chat.session.inProgress.local', "Local Agent");336} else if (chatSessionType === AgentSessionProviders.Background) {337displayName = localize('chat.session.inProgress.background', "Background Agent");338} else if (chatSessionType === AgentSessionProviders.Cloud) {339displayName = localize('chat.session.inProgress.cloud', "Cloud Agent");340} else {341displayName = this._contributions.get(chatSessionType)?.contribution.displayName;342}343344if (displayName) {345this.inProgressMap.set(displayName, count);346}347this._onDidChangeInProgress.fire();348}349350public getInProgress(): { displayName: string; count: number }[] {351return Array.from(this.inProgressMap.entries()).map(([displayName, count]) => ({ displayName, count }));352}353354private async updateInProgressStatus(chatSessionType: string): Promise<void> {355try {356const items = await this.getChatSessionItems(chatSessionType, CancellationToken.None);357const inProgress = items.filter(item => item.status && isSessionInProgressStatus(item.status));358this.reportInProgress(chatSessionType, inProgress.length);359} catch (error) {360this._logService.warn(`Failed to update in-progress status for chat session type '${chatSessionType}':`, error);361}362}363364private registerContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {365if (this._contributions.has(contribution.type)) {366return { dispose: () => { } };367}368369// Track context keys from the when condition370if (contribution.when) {371const whenExpr = ContextKeyExpr.deserialize(contribution.when);372if (whenExpr) {373for (const key of whenExpr.keys()) {374this._contextKeys.add(key);375}376}377}378379this._contributions.set(contribution.type, { contribution, extension: ext });380381// Register alternative IDs if provided382if (contribution.alternativeIds) {383for (const altId of contribution.alternativeIds) {384if (this._alternativeIdMap.has(altId)) {385this._logService.warn(`Alternative ID '${altId}' is already mapped to '${this._alternativeIdMap.get(altId)}'. Remapping to '${contribution.type}'.`);386}387this._alternativeIdMap.set(altId, contribution.type);388}389}390391// Store icon mapping if provided392let icon: ThemeIcon | { dark: URI; light: URI } | undefined;393394if (contribution.icon) {395// Parse icon string - support ThemeIcon format or file path from extension396if (typeof contribution.icon === 'string') {397icon = contribution.icon.startsWith('$(') && contribution.icon.endsWith(')')398? ThemeIcon.fromString(contribution.icon)399: ThemeIcon.fromId(contribution.icon);400} else {401icon = {402dark: resources.joinPath(ext.extensionLocation, contribution.icon.dark),403light: resources.joinPath(ext.extensionLocation, contribution.icon.light)404};405}406}407408if (icon) {409this._sessionTypeIcons.set(contribution.type, icon);410}411412// Store welcome title, message, tips, and input placeholder if provided413if (contribution.welcomeTitle) {414this._sessionTypeWelcomeTitles.set(contribution.type, contribution.welcomeTitle);415}416if (contribution.welcomeMessage) {417this._sessionTypeWelcomeMessages.set(contribution.type, contribution.welcomeMessage);418}419if (contribution.welcomeTips) {420this._sessionTypeWelcomeTips.set(contribution.type, contribution.welcomeTips);421}422if (contribution.inputPlaceholder) {423this._sessionTypeInputPlaceholders.set(contribution.type, contribution.inputPlaceholder);424}425426this._evaluateAvailability();427428return {429dispose: () => {430this._contributions.delete(contribution.type);431// Remove alternative ID mappings432if (contribution.alternativeIds) {433for (const altId of contribution.alternativeIds) {434if (this._alternativeIdMap.get(altId) === contribution.type) {435this._alternativeIdMap.delete(altId);436}437}438}439this._sessionTypeIcons.delete(contribution.type);440this._sessionTypeWelcomeTitles.delete(contribution.type);441this._sessionTypeWelcomeMessages.delete(contribution.type);442this._sessionTypeWelcomeTips.delete(contribution.type);443this._sessionTypeInputPlaceholders.delete(contribution.type);444this._contributionDisposables.deleteAndDispose(contribution.type);445this._updateHasCanDelegateProvidersContextKey();446}447};448}449450private _isContributionAvailable(contribution: IChatSessionsExtensionPoint): boolean {451if (!contribution.when) {452return true;453}454const whenExpr = ContextKeyExpr.deserialize(contribution.when);455return !whenExpr || this._contextKeyService.contextMatchesRules(whenExpr);456}457458/**459* Resolves a session type to its primary type, checking for alternative IDs.460* @param sessionType The session type or alternative ID to resolve461* @returns The primary session type, or undefined if not found or not available462*/463private _resolveToPrimaryType(sessionType: string): string | undefined {464// Try to find the primary type first465const contribution = this._contributions.get(sessionType)?.contribution;466if (contribution) {467// If the contribution is available, use it468if (this._isContributionAvailable(contribution)) {469return sessionType;470}471// If not available, fall through to check for alternatives472}473474// Check if this is an alternative ID, or if the primary type is not available475const primaryType = this._alternativeIdMap.get(sessionType);476if (primaryType) {477const altContribution = this._contributions.get(primaryType)?.contribution;478if (altContribution && this._isContributionAvailable(altContribution)) {479return primaryType;480}481}482483return undefined;484}485486private _registerMenuItems(contribution: IChatSessionsExtensionPoint, extensionDescription: IRelaxedExtensionDescription): IDisposable {487// If provider registers anything for the create submenu, let it fully control the creation488const contextKeyService = this._contextKeyService.createOverlay([489['chatSessionType', contribution.type]490]);491492const rawMenuActions = this._menuService.getMenuActions(MenuId.AgentSessionsCreateSubMenu, contextKeyService);493const menuActions = rawMenuActions.map(value => value[1]).flat();494495const disposables = new DisposableStore();496497// Mirror all create submenu actions into the global Chat New menu498for (const action of menuActions) {499if (action instanceof MenuItemAction) {500disposables.add(MenuRegistry.appendMenuItem(MenuId.ChatNewMenu, {501command: action.item,502group: '4_externally_contributed',503}));504}505}506return {507dispose: () => disposables.dispose()508};509}510511private _registerCommands(contribution: IChatSessionsExtensionPoint): IDisposable {512return combinedDisposable(513registerAction2(class OpenChatSessionAction extends Action2 {514constructor() {515super({516id: `workbench.action.chat.openSessionWithPrompt.${contribution.type}`,517title: localize2('interactiveSession.openSessionWithPrompt', "New {0} with Prompt", contribution.displayName),518category: CHAT_CATEGORY,519icon: Codicon.plus,520f1: false,521precondition: ChatContextKeys.enabled522});523}524525async run(accessor: ServicesAccessor, chatOptions?: { resource: UriComponents; prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {526const chatService = accessor.get(IChatService);527const { type } = contribution;528529if (chatOptions) {530const resource = URI.revive(chatOptions.resource);531const ref = await chatService.loadSessionForResource(resource, ChatAgentLocation.Chat, CancellationToken.None);532await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });533ref?.dispose();534}535}536}),537// Creates a chat editor538registerAction2(class OpenNewChatSessionEditorAction extends Action2 {539constructor() {540super({541id: `workbench.action.chat.openNewSessionEditor.${contribution.type}`,542title: localize2('interactiveSession.openNewSessionEditor', "New {0}", contribution.displayName),543category: CHAT_CATEGORY,544icon: Codicon.plus,545f1: true,546precondition: ChatContextKeys.enabled,547});548}549550async run(accessor: ServicesAccessor, chatOptions?: { prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {551const editorService = accessor.get(IEditorService);552const logService = accessor.get(ILogService);553const chatService = accessor.get(IChatService);554const { type } = contribution;555556try {557const options: IChatEditorOptions = {558override: ChatEditorInput.EditorID,559pinned: true,560title: {561fallback: localize('chatEditorContributionName', "{0}", contribution.displayName),562}563};564const resource = URI.from({565scheme: type,566path: `/untitled-${generateUuid()}`,567});568await editorService.openEditor({ resource, options });569if (chatOptions?.prompt) {570await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });571}572} catch (e) {573logService.error(`Failed to open new '${type}' chat session editor`, e);574}575}576}),577// New chat in sidebar chat (+ button)578registerAction2(class OpenNewChatSessionSidebarAction extends Action2 {579constructor() {580super({581id: `workbench.action.chat.openNewSessionSidebar.${contribution.type}`,582title: localize2('interactiveSession.openNewSessionSidebar', "New {0}", contribution.displayName),583category: CHAT_CATEGORY,584icon: Codicon.plus,585f1: false, // Hide from Command Palette586precondition: ChatContextKeys.enabled,587menu: {588id: MenuId.ChatNewMenu,589group: '3_new_special',590}591});592}593594async run(accessor: ServicesAccessor, chatOptions?: { prompt: string; attachedContext?: IChatRequestVariableEntry[] }): Promise<void> {595const viewsService = accessor.get(IViewsService);596const logService = accessor.get(ILogService);597const chatService = accessor.get(IChatService);598const { type } = contribution;599600try {601const resource = URI.from({602scheme: type,603path: `/untitled-${generateUuid()}`,604});605606const view = await viewsService.openView(ChatViewId) as ChatViewPane;607await view.loadSession(resource);608if (chatOptions?.prompt) {609await chatService.sendRequest(resource, chatOptions.prompt, { agentIdSilent: type, attachedContext: chatOptions.attachedContext });610}611view.focus();612} catch (e) {613logService.error(`Failed to open new '${type}' chat session in sidebar`, e);614}615}616})617);618}619620private _evaluateAvailability(): void {621let hasChanges = false;622for (const { contribution, extension } of this._contributions.values()) {623const isCurrentlyRegistered = this._contributionDisposables.has(contribution.type);624const shouldBeRegistered = this._isContributionAvailable(contribution);625if (isCurrentlyRegistered && !shouldBeRegistered) {626// Disable the contribution by disposing its disposable store627this._contributionDisposables.deleteAndDispose(contribution.type);628629// Also dispose any cached sessions for this contribution630this._disposeSessionsForContribution(contribution.type);631hasChanges = true;632} else if (!isCurrentlyRegistered && shouldBeRegistered) {633// Enable the contribution by registering it634this._enableContribution(contribution, extension);635hasChanges = true;636}637}638if (hasChanges) {639this._onDidChangeAvailability.fire();640for (const provider of this._itemsProviders.values()) {641this._onDidChangeItemsProviders.fire(provider);642}643for (const { contribution } of this._contributions.values()) {644this._onDidChangeSessionItems.fire(contribution.type);645}646}647this._updateHasCanDelegateProvidersContextKey();648}649650private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void {651const disposableStore = new DisposableStore();652this._contributionDisposables.set(contribution.type, disposableStore);653if (contribution.canDelegate) {654disposableStore.add(this._registerAgent(contribution, ext));655disposableStore.add(this._registerCommands(contribution));656}657disposableStore.add(this._registerMenuItems(contribution, ext));658}659660private _disposeSessionsForContribution(contributionId: string): void {661// Find and dispose all sessions that belong to this contribution662const sessionsToDispose: URI[] = [];663for (const [sessionResource, sessionData] of this._sessions) {664if (sessionData.chatSessionType === contributionId) {665sessionsToDispose.push(sessionResource);666}667}668669if (sessionsToDispose.length > 0) {670this._logService.info(`Disposing ${sessionsToDispose.length} cached sessions for contribution '${contributionId}' due to when clause change`);671}672673for (const sessionKey of sessionsToDispose) {674const sessionData = this._sessions.get(sessionKey);675if (sessionData) {676sessionData.dispose(); // This will call _onWillDisposeSession and clean up677}678}679}680681private _registerAgent(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): IDisposable {682const { type: id, name, displayName, description } = contribution;683const storedIcon = this._sessionTypeIcons.get(id);684const icons = ThemeIcon.isThemeIcon(storedIcon)685? { themeIcon: storedIcon, icon: undefined, iconDark: undefined }686: storedIcon687? { icon: storedIcon.light, iconDark: storedIcon.dark }688: { themeIcon: Codicon.sendToRemoteAgent };689690const agentData: IChatAgentData = {691id,692name,693fullName: displayName,694description: description,695isDefault: false,696isCore: false,697isDynamic: true,698slashCommands: contribution.commands ?? [],699locations: [ChatAgentLocation.Chat],700modes: [ChatModeKind.Agent, ChatModeKind.Ask],701disambiguation: [],702metadata: {703...icons,704},705capabilities: contribution.capabilities,706canAccessPreviousChatHistory: true,707extensionId: ext.identifier,708extensionVersion: ext.version,709extensionDisplayName: ext.displayName || ext.name,710extensionPublisherId: ext.publisher,711};712713return this._chatAgentService.registerAgent(id, agentData);714}715716getAllChatSessionContributions(): IChatSessionsExtensionPoint[] {717return Array.from(this._contributions.values(), x => x.contribution)718.filter(contribution => this._isContributionAvailable(contribution));719}720721private _updateHasCanDelegateProvidersContextKey(): void {722const hasCanDelegate = this.getAllChatSessionContributions().filter(c => c.canDelegate);723const canDelegateEnabled = hasCanDelegate.length > 0;724this._logService.trace(`[ChatSessionsService] hasCanDelegateProvidersAvailable=${canDelegateEnabled} (${hasCanDelegate.map(c => c.type).join(', ')})`);725this._hasCanDelegateProvidersKey.set(canDelegateEnabled);726}727728getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined {729const contribution = this._contributions.get(chatSessionType)?.contribution;730if (!contribution) {731return undefined;732}733734return this._isContributionAvailable(contribution) ? contribution : undefined;735}736737getAllChatSessionItemProviders(): IChatSessionItemProvider[] {738return [...this._itemsProviders.values()].filter(provider => {739// Check if the provider's corresponding contribution is available740const contribution = this._contributions.get(provider.chatSessionType)?.contribution;741return !contribution || this._isContributionAvailable(contribution);742});743}744745async activateChatSessionItemProvider(chatViewType: string): Promise<IChatSessionItemProvider | undefined> {746await this._extensionService.whenInstalledExtensionsRegistered();747const resolvedType = this._resolveToPrimaryType(chatViewType);748if (resolvedType) {749chatViewType = resolvedType;750}751752const contribution = this._contributions.get(chatViewType)?.contribution;753if (contribution && !this._isContributionAvailable(contribution)) {754return undefined;755}756757if (this._itemsProviders.has(chatViewType)) {758return this._itemsProviders.get(chatViewType);759}760761await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`);762763return this._itemsProviders.get(chatViewType);764}765766async canResolveChatSession(chatSessionResource: URI) {767await this._extensionService.whenInstalledExtensionsRegistered();768const resolvedType = this._resolveToPrimaryType(chatSessionResource.scheme) || chatSessionResource.scheme;769const contribution = this._contributions.get(resolvedType)?.contribution;770if (contribution && !this._isContributionAvailable(contribution)) {771return false;772}773774if (this._contentProviders.has(chatSessionResource.scheme)) {775return true;776}777778await this._extensionService.activateByEvent(`onChatSession:${chatSessionResource.scheme}`);779return this._contentProviders.has(chatSessionResource.scheme);780}781782async getAllChatSessionItems(token: CancellationToken): Promise<Array<{ readonly chatSessionType: string; readonly items: IChatSessionItem[] }>> {783return Promise.all(Array.from(this.getAllChatSessionContributions(), async contrib => {784return {785chatSessionType: contrib.type,786items: await this.getChatSessionItems(contrib.type, token)787};788}));789}790791private async getChatSessionItems(chatSessionType: string, token: CancellationToken): Promise<IChatSessionItem[]> {792if (!(await this.activateChatSessionItemProvider(chatSessionType))) {793return [];794}795796const resolvedType = this._resolveToPrimaryType(chatSessionType);797if (resolvedType) {798chatSessionType = resolvedType;799}800801const provider = this._itemsProviders.get(chatSessionType);802if (provider?.provideChatSessionItems) {803const sessions = await provider.provideChatSessionItems(token);804return sessions;805}806807return [];808}809810public registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable {811const chatSessionType = provider.chatSessionType;812this._itemsProviders.set(chatSessionType, provider);813this._onDidChangeItemsProviders.fire(provider);814815const disposables = new DisposableStore();816disposables.add(provider.onDidChangeChatSessionItems(() => {817this._onDidChangeSessionItems.fire(chatSessionType);818}));819820this.updateInProgressStatus(chatSessionType).catch(error => {821this._logService.warn(`Failed to update initial progress status for '${chatSessionType}':`, error);822});823824return {825dispose: () => {826disposables.dispose();827828const provider = this._itemsProviders.get(chatSessionType);829if (provider) {830this._itemsProviders.delete(chatSessionType);831this._onDidChangeItemsProviders.fire(provider);832}833}834};835}836837registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable {838if (this._contentProviders.has(chatSessionType)) {839throw new Error(`Content provider for ${chatSessionType} is already registered.`);840}841842this._contentProviders.set(chatSessionType, provider);843this._onDidChangeContentProviderSchemes.fire({ added: [chatSessionType], removed: [] });844845return {846dispose: () => {847this._contentProviders.delete(chatSessionType);848849this._onDidChangeContentProviderSchemes.fire({ added: [], removed: [chatSessionType] });850851// Remove all sessions that were created by this provider852for (const [key, session] of this._sessions) {853if (session.chatSessionType === chatSessionType) {854session.dispose();855this._sessions.delete(key);856}857}858}859};860}861862public registerChatModelChangeListeners(863chatService: IChatService,864chatSessionType: string,865onChange: () => void866): IDisposable {867const disposableStore = new DisposableStore();868const chatModelsICareAbout = chatService.chatModels.map(models =>869Array.from(models).filter((model: IChatModel) => model.sessionResource.scheme === chatSessionType)870);871872const listeners = new ResourceMap<IDisposable>();873const autoRunDisposable = autorunIterableDelta(874reader => chatModelsICareAbout.read(reader),875({ addedValues, removedValues }) => {876removedValues.forEach((removed) => {877const listener = listeners.get(removed.sessionResource);878if (listener) {879listeners.delete(removed.sessionResource);880listener.dispose();881}882});883addedValues.forEach((added) => {884const requestChangeListener = added.lastRequestObs.map(last => last?.response && observableSignalFromEvent('chatSessions.modelRequestChangeListener', last.response.onDidChange));885const modelChangeListener = observableSignalFromEvent('chatSessions.modelChangeListener', added.onDidChange);886listeners.set(added.sessionResource, autorun(reader => {887requestChangeListener.read(reader)?.read(reader);888modelChangeListener.read(reader);889onChange();890}));891});892}893);894disposableStore.add(toDisposable(() => {895for (const listener of listeners.values()) { listener.dispose(); }896}));897disposableStore.add(autoRunDisposable);898return disposableStore;899}900901902public getInProgressSessionDescription(chatModel: IChatModel): string | undefined {903const requests = chatModel.getRequests();904if (requests.length === 0) {905return undefined;906}907908// Get the last request to check its response status909const lastRequest = requests.at(-1);910const response = lastRequest?.response;911if (!response) {912return undefined;913}914915// If the response is complete, show Finished916if (response.isComplete) {917return undefined;918}919920// Get the response parts to find tool invocations and progress messages921const responseParts = response.response.value;922let description: string | IMarkdownString | undefined = '';923924for (let i = responseParts.length - 1; i >= 0; i--) {925const part = responseParts[i];926if (description) {927break;928}929930if (part.kind === 'confirmation' && typeof part.message === 'string') {931description = part.message;932} else if (part.kind === 'toolInvocation') {933const toolInvocation = part as IChatToolInvocation;934const state = toolInvocation.state.get();935description = toolInvocation.generatedTitle || toolInvocation.pastTenseMessage || toolInvocation.invocationMessage;936if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {937const confirmationTitle = toolInvocation.confirmationMessages?.title;938const titleMessage = confirmationTitle && (typeof confirmationTitle === 'string'939? confirmationTitle940: confirmationTitle.value);941const descriptionValue = typeof description === 'string' ? description : description.value;942description = titleMessage ?? localize('chat.sessions.description.waitingForConfirmation', "Waiting for confirmation: {0}", descriptionValue);943}944} else if (part.kind === 'toolInvocationSerialized') {945description = part.invocationMessage;946} else if (part.kind === 'progressMessage') {947description = part.content;948} else if (part.kind === 'thinking') {949description = localize('chat.sessions.description.thinking', 'Thinking...');950}951}952953return renderAsPlaintext(description, { useLinkFormatter: true });954}955956public async getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise<IChatSession> {957const existingSessionData = this._sessions.get(sessionResource);958if (existingSessionData) {959return existingSessionData.session;960}961962if (!(await raceCancellationError(this.canResolveChatSession(sessionResource), token))) {963throw Error(`Can not find provider for ${sessionResource}`);964}965966const resolvedType = this._resolveToPrimaryType(sessionResource.scheme) || sessionResource.scheme;967const provider = this._contentProviders.get(resolvedType);968if (!provider) {969throw Error(`Can not find provider for ${sessionResource}`);970}971972const session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token);973const sessionData = new ContributedChatSessionData(session, sessionResource.scheme, sessionResource, session.options, resource => {974sessionData.dispose();975this._sessions.delete(resource);976});977978this._sessions.set(sessionResource, sessionData);979980return session;981}982983public hasAnySessionOptions(sessionResource: URI): boolean {984const session = this._sessions.get(sessionResource);985return !!session && !!session.options && Object.keys(session.options).length > 0;986}987988public getSessionOption(sessionResource: URI, optionId: string): string | IChatSessionProviderOptionItem | undefined {989const session = this._sessions.get(sessionResource);990return session?.getOption(optionId);991}992993public setSessionOption(sessionResource: URI, optionId: string, value: string | IChatSessionProviderOptionItem): boolean {994const session = this._sessions.get(sessionResource);995return !!session?.setOption(optionId, value);996}997998public notifySessionItemsChanged(chatSessionType: string): void {999this._onDidChangeSessionItems.fire(chatSessionType);1000}10011002/**1003* Store option groups for a session type1004*/1005public setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void {1006if (optionGroups) {1007this._sessionTypeOptions.set(chatSessionType, optionGroups);1008} else {1009this._sessionTypeOptions.delete(chatSessionType);1010}1011this._onDidChangeOptionGroups.fire(chatSessionType);1012}10131014/**1015* Get available option groups for a session type1016*/1017public getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined {1018return this._sessionTypeOptions.get(chatSessionType);1019}10201021private _optionsChangeCallback?: SessionOptionsChangedCallback;10221023/**1024* Set the callback for notifying extensions about option changes1025*/1026public setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void {1027this._optionsChangeCallback = callback;1028}10291030/**1031* Notify extension about option changes for a session1032*/1033public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise<void> {1034if (!updates.length) {1035return;1036}1037if (this._optionsChangeCallback) {1038await this._optionsChangeCallback(sessionResource, updates);1039}1040for (const u of updates) {1041this.setSessionOption(sessionResource, u.optionId, u.value);1042}1043this._onDidChangeSessionOptions.fire(sessionResource);1044}10451046/**1047* Get the icon for a specific session type1048*/1049public getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined {1050const sessionTypeIcon = this._sessionTypeIcons.get(chatSessionType);10511052if (ThemeIcon.isThemeIcon(sessionTypeIcon)) {1053return sessionTypeIcon;1054}10551056if (isDark(this._themeService.getColorTheme().type)) {1057return sessionTypeIcon?.dark;1058} else {1059return sessionTypeIcon?.light;1060}1061}10621063/**1064* Get the welcome title for a specific session type1065*/1066public getWelcomeTitleForSessionType(chatSessionType: string): string | undefined {1067return this._sessionTypeWelcomeTitles.get(chatSessionType);1068}10691070/**1071* Get the welcome message for a specific session type1072*/1073public getWelcomeMessageForSessionType(chatSessionType: string): string | undefined {1074return this._sessionTypeWelcomeMessages.get(chatSessionType);1075}10761077/**1078* Get the input placeholder for a specific session type1079*/1080public getInputPlaceholderForSessionType(chatSessionType: string): string | undefined {1081return this._sessionTypeInputPlaceholders.get(chatSessionType);1082}10831084/**1085* Get the capabilities for a specific session type1086*/1087public getCapabilitiesForSessionType(chatSessionType: string): IChatAgentAttachmentCapabilities | undefined {1088const contribution = this._contributions.get(chatSessionType)?.contribution;1089return contribution?.capabilities;1090}10911092public getContentProviderSchemes(): string[] {1093return Array.from(this._contentProviders.keys());1094}1095}10961097registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);109810991100