diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 64ebe94abd9..3b85c248ef2 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -87,7 +87,11 @@ export namespace Schemas { /** Scheme used for the chat input part */ export const vscodeChatInput = 'chatSessionInput'; - /** Scheme for chat session content */ + /** + * Scheme for chat session content + * + * @deprecated + * */ export const vscodeChatSession = 'vscode-chat-session'; /** diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 79e8cfa17c0..3a9295dcd16 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -62,7 +62,7 @@ const _allApiProposals = { }, chatSessionsProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts', - version: 2 + version: 3 }, chatStatusItem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts', diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 5e0587c5560..bee6b357b78 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -8,34 +8,26 @@ import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter } from '../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../base/common/map.js'; import { revive } from '../../../base/common/marshalling.js'; import { autorun, IObservable, observableValue } from '../../../base/common/observable.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IDialogService } from '../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../platform/log/common/log.js'; -import { ChatViewId } from '../../contrib/chat/browser/chat.js'; -import { ChatViewPane } from '../../contrib/chat/browser/chatViewPane.js'; import { IChatAgentRequest } from '../../contrib/chat/common/chatAgents.js'; import { IChatContentInlineReference, IChatProgress } from '../../contrib/chat/common/chatService.js'; import { ChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; -import { ChatSessionUri } from '../../contrib/chat/common/chatUri.js'; -import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { IEditorGroup, IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../services/editor/common/editorService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import { IViewsService } from '../../services/views/common/viewsService.js'; import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatVariableEntries.js'; import { ExtHostChatSessionsShape, ExtHostContext, IChatProgressDto, IChatSessionHistoryItemDto, MainContext, MainThreadChatSessionsShape } from '../common/extHost.protocol.js'; import { IChatEditorOptions } from '../../contrib/chat/browser/chatEditor.js'; export class ObservableChatSession extends Disposable implements ChatSession { - static generateSessionKey(providerHandle: number, sessionId: string) { - return `${providerHandle}_${sessionId}`; - } - readonly sessionId: string; readonly sessionResource: URI; readonly providerHandle: number; readonly history: Array; @@ -69,10 +61,6 @@ export class ObservableChatSession extends Disposable implements ChatSession { private readonly _logService: ILogService; private readonly _dialogService: IDialogService; - get sessionKey(): string { - return ObservableChatSession.generateSessionKey(this.providerHandle, this.sessionId); - } - get progressObs(): IObservable { return this._progressObservable; } @@ -82,7 +70,6 @@ export class ObservableChatSession extends Disposable implements ChatSession { } constructor( - id: string, resource: URI, providerHandle: number, proxy: ExtHostChatSessionsShape, @@ -91,7 +78,6 @@ export class ObservableChatSession extends Disposable implements ChatSession { ) { super(); - this.sessionId = id; this.sessionResource = resource; this.providerHandle = providerHandle; this.history = []; @@ -111,7 +97,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { private async _doInitializeContent(token: CancellationToken): Promise { try { const sessionContent = await raceCancellationError( - this._proxy.$provideChatSessionContent(this._providerHandle, this.sessionId, this.sessionResource, token), + this._proxy.$provideChatSessionContent(this._providerHandle, this.sessionResource, token), token ); @@ -147,10 +133,10 @@ export class ObservableChatSession extends Disposable implements ChatSession { this.interruptActiveResponseCallback = async () => { const confirmInterrupt = () => { if (this._disposalPending) { - this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionId); + this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionResource); this._disposalPending = false; } - this._proxy.$interruptChatSessionActiveResponse(this._providerHandle, this.sessionId, 'ongoing'); + this._proxy.$interruptChatSessionActiveResponse(this._providerHandle, this.sessionResource, 'ongoing'); return true; }; @@ -177,7 +163,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { this._interruptionWasCanceled = true; // User canceled interruption - cancel the deferred disposal if (this._disposalPending) { - this._logService.info(`Canceling deferred disposal for session ${this.sessionId} (user canceled interruption)`); + this._logService.info(`Canceling deferred disposal for session ${this.sessionResource} (user canceled interruption)`); this._disposalPending = false; } return false; @@ -215,7 +201,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { }); try { - await this._proxy.$invokeChatSessionRequestHandler(this._providerHandle, this.sessionId, request, history, token); + await this._proxy.$invokeChatSessionRequestHandler(this._providerHandle, this.sessionResource, request, history, token); // Only mark as complete if there's no active response callback // Sessions with active response callbacks should only complete when explicitly told to via handleProgressComplete @@ -246,7 +232,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { const hasAnyCapability = hasActiveResponse || hasRequestHandler; for (const [requestId, chunks] of this._pendingProgressChunks) { - this._logService.debug(`Processing ${chunks.length} pending progress chunks for session ${this.sessionId}, requestId ${requestId}`); + this._logService.debug(`Processing ${chunks.length} pending progress chunks for session ${this.sessionResource}, requestId ${requestId}`); this._addProgress(chunks); } this._pendingProgressChunks.clear(); @@ -257,7 +243,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { } } catch (error) { - this._logService.error(`Failed to initialize chat session ${this.sessionId}:`, error); + this._logService.error(`Failed to initialize chat session ${this.sessionResource}:`, error); throw error; } } @@ -270,7 +256,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { if (!this._isInitialized) { const existing = this._pendingProgressChunks.get(requestId) || []; this._pendingProgressChunks.set(requestId, [...existing, ...progress]); - this._logService.debug(`Queuing ${progress.length} progress chunks for session ${this.sessionId}, requestId ${requestId} (session not initialized)`); + this._logService.debug(`Queuing ${progress.length} progress chunks for session ${this.sessionResource}, requestId ${requestId} (session not initialized)`); return; } @@ -318,7 +304,7 @@ export class ObservableChatSession extends Disposable implements ChatSession { // The actual disposal will happen in the interruption callback based on user's choice } else { // No active response callback or user already canceled interruption - dispose immediately - this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionId); + this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionResource); } super.dispose(); } @@ -333,8 +319,8 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat private readonly _contentProvidersRegistrations = this._register(new DisposableMap()); private readonly _sessionTypeToHandle = new Map(); - private readonly _activeSessions = new Map(); - private readonly _sessionDisposables = new Map(); + private readonly _activeSessions = new ResourceMap(); + private readonly _sessionDisposables = new ResourceMap(); private readonly _proxy: ExtHostChatSessionsShape; @@ -345,16 +331,15 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat @IEditorService private readonly _editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @ILogService private readonly _logService: ILogService, - @IViewsService private readonly _viewsService: IViewsService, ) { super(); this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions); - this._chatSessionsService.setOptionsChangeCallback(async (chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>) => { + this._chatSessionsService.setOptionsChangeCallback(async (chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => { const handle = this._getHandleForSessionType(chatSessionType); if (handle !== undefined) { - await this.notifyOptionsChange(handle, sessionId, updates); + await this.notifyOptionsChange(handle, sessionResource, updates); } }); } @@ -387,15 +372,17 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire(); } - $onDidCommitChatSessionItem(handle: number, original: { id: string; resource: UriComponents }, modified: { id: string; resource: UriComponents }): void { - this._logService.trace(`$onDidCommitChatSessionItem: handle(${handle}), original(${original}), modified(${modified})`); + $onDidCommitChatSessionItem(handle: number, originalComponents: UriComponents, modifiedCompoennts: UriComponents): void { + const originalResource = URI.revive(originalComponents); + const modifiedResource = URI.revive(modifiedCompoennts); + + this._logService.trace(`$onDidCommitChatSessionItem: handle(${handle}), original(${originalResource}), modified(${modifiedResource})`); const chatSessionType = this._itemProvidersRegistrations.get(handle)?.provider.chatSessionType; if (!chatSessionType) { this._logService.error(`No chat session type found for provider handle ${handle}`); return; } - const originalResource = ChatSessionUri.forSession(chatSessionType, original.id); - const modifiedResource = ChatSessionUri.forSession(chatSessionType, modified.id); + const originalEditor = this._editorService.editors.find(editor => editor.resource?.toString() === originalResource.toString()); const contribution = this._chatSessionsService.getAllChatSessionContributions().find(c => c.type === chatSessionType); @@ -421,9 +408,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat if (originalEditor) { // Prefetch the chat session content to make the subsequent editor swap quick this._chatSessionsService.provideChatSessionContent( - chatSessionType, - modified.id, - URI.revive(modified.resource), + URI.revive(modifiedResource), CancellationToken.None, ).then(() => { this._editorService.replaceEditors([{ @@ -476,26 +461,24 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat } } - private async _provideChatSessionContent(providerHandle: number, id: string, resource: URI, token: CancellationToken): Promise { - const sessionKey = ObservableChatSession.generateSessionKey(providerHandle, id); - let session = this._activeSessions.get(sessionKey); + private async _provideChatSessionContent(providerHandle: number, sessionResource: URI, token: CancellationToken): Promise { + let session = this._activeSessions.get(sessionResource); if (!session) { session = new ObservableChatSession( - id, - resource, + sessionResource, providerHandle, this._proxy, this._logService, this._dialogService ); - this._activeSessions.set(sessionKey, session); + this._activeSessions.set(sessionResource, session); const disposable = session.onWillDispose(() => { - this._activeSessions.delete(sessionKey); - this._sessionDisposables.get(sessionKey)?.dispose(); - this._sessionDisposables.delete(sessionKey); + this._activeSessions.delete(sessionResource); + this._sessionDisposables.get(sessionResource)?.dispose(); + this._sessionDisposables.delete(sessionResource); }); - this._sessionDisposables.set(sessionKey, disposable); + this._sessionDisposables.set(sessionResource, disposable); } try { @@ -504,7 +487,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat for (const [chatSessionType, handle] of this._sessionTypeToHandle) { if (handle === providerHandle) { for (const [optionId, value] of Object.entries(session.options)) { - this._chatSessionsService.setSessionOption(chatSessionType, id, optionId, value); + this._chatSessionsService.setSessionOption(chatSessionType, sessionResource, optionId, value); } break; } @@ -514,7 +497,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat return session; } catch (error) { session.dispose(); - this._logService.error(`Error providing chat session content for handle ${providerHandle} and id ${id}:`, error); + this._logService.error(`Error providing chat session content for handle ${providerHandle} and resource ${sessionResource.toString()}:`, error); throw error; } } @@ -523,16 +506,16 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._itemProvidersRegistrations.deleteAndDispose(handle); } - $registerChatSessionContentProvider(handle: number, chatSessionType: string): void { + $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void { const provider: IChatSessionContentProvider = { - provideChatSessionContent: (id, resource, token) => this._provideChatSessionContent(handle, id, resource, token) + provideChatSessionContent: (resource, token) => this._provideChatSessionContent(handle, resource, token) }; - this._sessionTypeToHandle.set(chatSessionType, handle); - this._contentProvidersRegistrations.set(handle, this._chatSessionsService.registerChatSessionContentProvider(chatSessionType, provider)); + this._sessionTypeToHandle.set(chatSessionScheme, handle); + this._contentProvidersRegistrations.set(handle, this._chatSessionsService.registerChatSessionContentProvider(chatSessionScheme, provider)); this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => { if (options?.optionGroups && options.optionGroups.length) { - this._chatSessionsService.setOptionGroupsForSessionType(chatSessionType, handle, options.optionGroups); + this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, options.optionGroups); } }).catch(err => this._logService.error('Error fetching chat session options', err)); } @@ -555,34 +538,34 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat } } - async $handleProgressChunk(handle: number, sessionId: string, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise { - const sessionKey = ObservableChatSession.generateSessionKey(handle, sessionId); - const observableSession = this._activeSessions.get(sessionKey); + async $handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise { + const resource = URI.revive(sessionResource); + const observableSession = this._activeSessions.get(resource); + if (!observableSession) { + this._logService.warn(`No session found for progress chunks: handle ${handle}, sessionResource ${resource}, requestId ${requestId}`); + return; + } const chatProgressParts: IChatProgress[] = chunks.map(chunk => { const [progress] = Array.isArray(chunk) ? chunk : [chunk]; return revive(progress) as IChatProgress; }); - if (observableSession) { - observableSession.handleProgressChunk(requestId, chatProgressParts); - } else { - this._logService.warn(`No session found for progress chunks: handle ${handle}, sessionId ${sessionId}, requestId ${requestId}`); - } + observableSession.handleProgressChunk(requestId, chatProgressParts); } - $handleProgressComplete(handle: number, sessionId: string, requestId: string) { - const sessionKey = ObservableChatSession.generateSessionKey(handle, sessionId); - const observableSession = this._activeSessions.get(sessionKey); - - if (observableSession) { - observableSession.handleProgressComplete(requestId); - } else { - this._logService.warn(`No session found for progress completion: handle ${handle}, sessionId ${sessionId}, requestId ${requestId}`); + $handleProgressComplete(handle: number, sessionResource: UriComponents, requestId: string) { + const resource = URI.revive(sessionResource); + const observableSession = this._activeSessions.get(resource); + if (!observableSession) { + this._logService.warn(`No session found for progress completion: handle ${handle}, sessionResource ${resource}, requestId ${requestId}`); + return; } + + observableSession.handleProgressComplete(requestId); } - $handleAnchorResolve(handle: number, sessionId: string, requestId: string, requestHandle: string, anchor: Dto): void { + $handleAnchorResolve(handle: number, sesssionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto): void { // throw new Error('Method not implemented.'); } @@ -618,28 +601,14 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat return undefined; } - async $showChatSession(chatSessionType: string, sessionId: string, position: EditorGroupColumn | undefined): Promise { - const sessionUri = ChatSessionUri.forSession(chatSessionType, sessionId); - - if (typeof position === 'undefined') { - const chatPanel = await this._viewsService.openView(ChatViewId); - await chatPanel?.loadSession(sessionUri); - } else { - await this._editorService.openEditor({ - resource: sessionUri, - options: { pinned: true }, - }, position); - } - } - /** * Notify the extension about option changes for a session */ - async notifyOptionsChange(handle: number, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>): Promise { + async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>): Promise { try { - await this._proxy.$provideHandleOptionsChange(handle, sessionId, updates, CancellationToken.None); + await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None); } catch (error) { - this._logService.error(`Error notifying extension about options change for handle ${handle}, sessionId ${sessionId}:`, error); + this._logService.error(`Error notifying extension about options change for handle ${handle}, sessionResource ${sessionResource}:`, error); } } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d3482f995c8..27eaa67856f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -969,10 +969,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatStatusItem'); return extHostChatStatus.createChatStatusItem(extension, id); }, - showChatSession: (chatSessionType: string, sessionId: string, options?: vscode.ChatSessionShowOptions) => { - checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.showChatSession(extension, chatSessionType, sessionId, options); - }, }; // namespace: workspace @@ -1530,9 +1526,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatSessionsProvider'); return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider); }, - registerChatSessionContentProvider(chatSessionType: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) { + registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) { checkProposedApiEnabled(extension, 'chatSessionsProvider'); - return extHostChatSessions.registerChatSessionContentProvider(extension, chatSessionType, chatParticipant, provider, capabilities); + return extHostChatSessions.registerChatSessionContentProvider(extension, scheme, chatParticipant, provider, capabilities); }, registerChatOutputRenderer: (viewType: string, renderer: vscode.ChatOutputRenderer) => { checkProposedApiEnabled(extension, 'chatOutputRenderer'); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index f640cf8e4a5..44cc8e71bbe 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -58,7 +58,7 @@ import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, UserSelectedTo import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import { IChatRelatedFile, IChatRelatedFileProviderMetadata as IChatRelatedFilesProviderMetadata, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatProgressHistoryResponseContent, IChatRequestVariableData } from '../../contrib/chat/common/chatModel.js'; -import { ChatResponseClearToPreviousToolInvocationReason, IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatSessionContext, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; +import { ChatResponseClearToPreviousToolInvocationReason, IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { IChatSessionItem, IChatSessionProviderOptionGroup } from '../../contrib/chat/common/chatSessionsService.js'; import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; @@ -1388,8 +1388,15 @@ export type IChatAgentHistoryEntryDto = { result: IChatAgentResult; }; +export interface IChatSessionContextDto { + readonly chatSessionType: string; + readonly chatSessionId: string; + readonly chatSessionResource: UriComponents; + readonly isUntitled: boolean; +} + export interface ExtHostChatAgentsShape2 { - $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContext; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise; + $invokeAgent(handle: number, request: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise; $provideFollowups(request: Dto, handle: number, result: IChatAgentResult, context: { history: IChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; $acceptFeedback(handle: number, result: IChatAgentResult, voteAction: IChatVoteAction): void; $acceptAction(handle: number, result: IChatAgentResult, action: IChatUserActionEvent): void; @@ -3203,26 +3210,25 @@ export interface MainThreadChatSessionsShape extends IDisposable { $registerChatSessionItemProvider(handle: number, chatSessionType: string): void; $unregisterChatSessionItemProvider(handle: number): void; $onDidChangeChatSessionItems(handle: number): void; - $onDidCommitChatSessionItem(handle: number, original: { id: string; resource: UriComponents }, modified: { id: string; resource: UriComponents }): void; - $registerChatSessionContentProvider(handle: number, chatSessionType: string): void; + $onDidCommitChatSessionItem(handle: number, original: UriComponents, modified: UriComponents): void; + $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void; $unregisterChatSessionContentProvider(handle: number): void; - $handleProgressChunk(handle: number, sessionId: string, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise; - $handleAnchorResolve(handle: number, sessionId: string, requestId: string, requestHandle: string, anchor: Dto): void; - $handleProgressComplete(handle: number, sessionId: string, requestId: string): void; - - $showChatSession(chatSessionType: string, sessionId: string, position: EditorGroupColumn | undefined): Promise; + $handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise; + $handleAnchorResolve(handle: number, sessionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto): void; + $handleProgressComplete(handle: number, sessionResource: UriComponents, requestId: string): void; } export interface ExtHostChatSessionsShape { $provideChatSessionItems(providerHandle: number, token: CancellationToken): Promise[]>; $provideNewChatSessionItem(providerHandle: number, options: { request: IChatAgentRequest; metadata?: any }, token: CancellationToken): Promise>; - $provideChatSessionContent(providerHandle: number, sessionId: string, sessionResource: UriComponents, token: CancellationToken): Promise; + + $provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, token: CancellationToken): Promise; + $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise; + $disposeChatSessionContent(providerHandle: number, sessionResource: UriComponents): Promise; + $invokeChatSessionRequestHandler(providerHandle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise; $provideChatSessionProviderOptions(providerHandle: number, token: CancellationToken): Promise; - $provideHandleOptionsChange(providerHandle: number, sessionId: string, updates: ReadonlyArray, token: CancellationToken): Promise; - $interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise; - $disposeChatSessionContent(providerHandle: number, sessionId: string): Promise; - $invokeChatSessionRequestHandler(providerHandle: number, id: string, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise; + $provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: ReadonlyArray, token: CancellationToken): Promise; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 9c6ee6d866c..d827de9e1bb 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -22,11 +22,11 @@ import { ILogService } from '../../../platform/log/common/log.js'; import { isChatViewTitleActionContext } from '../../contrib/chat/common/chatActions.js'; import { IChatAgentRequest, IChatAgentResult, IChatAgentResultTimings, UserSelectedTools } from '../../contrib/chat/common/chatAgents.js'; import { IChatRelatedFile, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; -import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatSessionContext, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; +import { ChatAgentVoteDirection, IChatContentReference, IChatFollowup, IChatResponseErrorDetails, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js'; +import { ExtHostChatAgentsShape2, IChatAgentCompletionItem, IChatAgentHistoryEntryDto, IChatAgentProgressShape, IChatProgressDto, IChatSessionContextDto, IExtensionChatAgentMetadata, IMainContext, MainContext, MainThreadChatAgentsShape2 } from './extHost.protocol.js'; import { CommandsConverter, ExtHostCommands } from './extHostCommands.js'; import { ExtHostDiagnostics } from './extHostDiagnostics.js'; import { ExtHostDocuments } from './extHostDocuments.js'; @@ -541,7 +541,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS this._onDidChangeChatRequestTools.fire(request.extRequest); } - async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContext; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise { + async $invokeAgent(handle: number, requestDto: Dto, context: { history: IChatAgentHistoryEntryDto[]; chatSessionContext?: IChatSessionContextDto; chatSummary?: { prompt?: string; history?: string } }, token: CancellationToken): Promise { const agent = this._agents.get(handle); if (!agent) { throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); @@ -581,7 +581,6 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS if (context.chatSessionContext) { chatSessionContext = { chatSessionItem: { - id: context.chatSessionContext.chatSessionId, resource: URI.revive(context.chatSessionContext.chatSessionResource), label: context.chatSessionContext.isUntitled ? 'Untitled Session' : 'Session', }, diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 192f9dc7918..f6dd54e1589 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -6,9 +6,12 @@ import type * as vscode from 'vscode'; import { coalesce } from '../../../base/common/arrays.js'; import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js'; +import { CancellationError } from '../../../base/common/errors.js'; import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../base/common/map.js'; import { revive } from '../../../base/common/marshalling.js'; import { MarshalledId } from '../../../base/common/marshallingIds.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; @@ -22,8 +25,6 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConvert from './extHostTypeConverters.js'; import * as extHostTypes from './extHostTypes.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; -import { ChatSessionUri } from '../../contrib/chat/common/chatUri.js'; class ExtHostChatSession { private _stream: ChatAgentResponseStream; @@ -66,7 +67,19 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }>(); private _nextChatSessionItemProviderHandle = 0; private _nextChatSessionContentProviderHandle = 0; - private readonly _sessionMap: Map = new Map(); + + /** + * Map of uri -> chat session items + * + * TODO: this isn't cleared/updated properly + */ + private readonly _sessionItems = new ResourceMap(); + + /** + * Map of uri -> chat sessions infos + */ + private readonly _extHostChatSessions = new ResourceMap<{ readonly sessionObj: ExtHostChatSession; readonly disposeCts: CancellationTokenSource }>(); + constructor( private readonly commands: ExtHostCommands, @@ -81,7 +94,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio processArgument: (arg) => { if (arg && arg.$mid === MarshalledId.ChatSessionContext) { const id = arg.session.id; - const sessionContent = this._sessionMap.get(id); + const sessionContent = this._sessionItems.get(id); if (sessionContent) { return sessionContent; } else { @@ -109,9 +122,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio if (provider.onDidCommitChatSessionItem) { disposables.add(provider.onDidCommitChatSessionItem((e) => { const { original, modified } = e; - this._proxy.$onDidCommitChatSessionItem(handle, - { id: original.id, resource: original.resource ?? ChatSessionUri.forSession(chatSessionType, original.id) }, - { id: modified.id, resource: modified.resource ?? ChatSessionUri.forSession(chatSessionType, modified.id) }); + this._proxy.$onDidCommitChatSessionItem(handle, original.resource, modified.resource); })); } return { @@ -123,12 +134,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionType: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable { + registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionScheme: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable { const handle = this._nextChatSessionContentProviderHandle++; const disposables = new DisposableStore(); this._chatSessionContentProviders.set(handle, { provider, extension, capabilities, disposable: disposables }); - this._proxy.$registerChatSessionContentProvider(handle, chatSessionType); + this._proxy.$registerChatSessionContentProvider(handle, chatSessionScheme); return new extHostTypes.Disposable(() => { this._chatSessionContentProviders.delete(handle); @@ -137,14 +148,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }); } - async showChatSession(_extension: IExtensionDescription, chatSessionType: string, sessionId: string, options: vscode.ChatSessionShowOptions | undefined): Promise { - await this._proxy.$showChatSession(chatSessionType, sessionId, typeConvert.ViewColumn.from(options?.viewColumn)); - } - private convertChatSessionStatus(status: vscode.ChatSessionStatus | undefined): ChatSessionStatus | undefined { if (status === undefined) { return undefined; } + switch (status) { case 0: // vscode.ChatSessionStatus.Failed return ChatSessionStatus.Failed; @@ -159,8 +167,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio private convertChatSessionItem(sessionType: string, sessionContent: vscode.ChatSessionItem): IChatSessionItem { return { - id: sessionContent.id, - resource: sessionContent.resource ?? ChatSessionUri.forSession(sessionType, sessionContent.id), + id: sessionContent.resource.toString(), + resource: sessionContent.resource, label: sessionContent.label, description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined, status: this.convertChatSessionStatus(sessionContent.status), @@ -199,13 +207,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; const chatSessionItem = await entry.provider.provideNewChatSessionItem(vscodeOptions, token); - if (!chatSessionItem || !chatSessionItem.id) { + if (!chatSessionItem) { throw new Error('Provider did not create session'); } - this._sessionMap.set( - chatSessionItem.id, - chatSessionItem - ); + + this._sessionItems.set(chatSessionItem.resource, chatSessionItem); return this.convertChatSessionItem(entry.sessionType, chatSessionItem); } catch (error) { this._logService.error(`Error creating chat session: ${error}`); @@ -227,29 +233,28 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const response: IChatSessionItem[] = []; for (const sessionContent of sessions) { - if (sessionContent.id) { - this._sessionMap.set( - sessionContent.id, - sessionContent - ); - response.push(this.convertChatSessionItem(entry.sessionType, sessionContent)); - } + this._sessionItems.set(sessionContent.resource, sessionContent); + response.push(this.convertChatSessionItem(entry.sessionType, sessionContent)); } return response; } - private readonly _extHostChatSessions = new Map(); - - async $provideChatSessionContent(handle: number, id: string, resource: UriComponents, token: CancellationToken): Promise { + async $provideChatSessionContent(handle: number, sessionResourceComponents: UriComponents, token: CancellationToken): Promise { const provider = this._chatSessionContentProviders.get(handle); if (!provider) { throw new Error(`No provider for handle ${handle}`); } - const session = await provider.provider.provideChatSessionContent(id, token); + const sessionResource = URI.revive(sessionResourceComponents); + + const session = await provider.provider.provideChatSessionContent(sessionResource, token); + if (token.isCancellationRequested) { + throw new CancellationError(); + } const sessionDisposables = new DisposableStore(); const sessionId = ExtHostChatSessions._sessionHandlePool++; + const id = sessionResource.toString(); const chatSession = new ExtHostChatSession(session, provider.extension, { sessionId: `${id}.${sessionId}`, requestId: 'ongoing', @@ -259,27 +264,27 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio location: ChatAgentLocation.Chat, }, { $handleProgressChunk: (requestId, chunks) => { - return this._proxy.$handleProgressChunk(handle, id, requestId, chunks); + return this._proxy.$handleProgressChunk(handle, sessionResource, requestId, chunks); }, $handleAnchorResolve: (requestId, requestHandle, anchor) => { - this._proxy.$handleAnchorResolve(handle, id, requestId, requestHandle, anchor); + this._proxy.$handleAnchorResolve(handle, sessionResource, requestId, requestHandle, anchor); }, }, this.commands.converter, sessionDisposables); const disposeCts = sessionDisposables.add(new CancellationTokenSource()); - this._extHostChatSessions.set(`${handle}_${id}`, { sessionObj: chatSession, disposeCts }); + this._extHostChatSessions.set(sessionResource, { sessionObj: chatSession, disposeCts }); // Call activeResponseCallback immediately for best user experience if (session.activeResponseCallback) { Promise.resolve(session.activeResponseCallback(chatSession.activeResponseStream.apiObject, disposeCts.token)).finally(() => { // complete - this._proxy.$handleProgressComplete(handle, id, 'ongoing'); + this._proxy.$handleProgressComplete(handle, sessionResource, 'ongoing'); }); } const { capabilities } = provider; return { id: sessionId + '', - resource: URI.revive(resource), + resource: URI.revive(sessionResource), hasActiveResponseCallback: !!session.activeResponseCallback, hasRequestHandler: !!session.requestHandler, supportsInterruption: !!capabilities?.supportsInterruptions, @@ -294,7 +299,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - async $provideHandleOptionsChange(handle: number, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>, token: CancellationToken): Promise { + async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>, token: CancellationToken): Promise { + const sessionResource = URI.revive(sessionResourceComponents); const provider = this._chatSessionContentProviders.get(handle); if (!provider) { this._logService.warn(`No provider for handle ${handle}`); @@ -307,9 +313,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } try { - await provider.provider.provideHandleOptionsChange(sessionId, updates, token); + await provider.provider.provideHandleOptionsChange(sessionResource, updates, token); } catch (error) { - this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionId ${sessionId}:`, error); + this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionResource ${sessionResource}:`, error); } } @@ -339,27 +345,25 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } } - async $interruptChatSessionActiveResponse(providerHandle: number, sessionId: string, requestId: string): Promise { - const key = `${providerHandle}_${sessionId}`; - const entry = this._extHostChatSessions.get(key); + async $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise { + const entry = this._extHostChatSessions.get(URI.revive(sessionResource)); entry?.disposeCts.cancel(); } - async $disposeChatSessionContent(providerHandle: number, sessionId: string): Promise { - const key = `${providerHandle}_${sessionId}`; - const entry = this._extHostChatSessions.get(key); + async $disposeChatSessionContent(providerHandle: number, sessionResource: UriComponents): Promise { + const entry = this._extHostChatSessions.get(URI.revive(sessionResource)); if (!entry) { - this._logService.warn(`No chat session found for ID: ${key}`); + this._logService.warn(`No chat session found for resource: ${sessionResource}`); return; } entry.disposeCts.cancel(); entry.sessionObj.sessionDisposables.dispose(); - this._extHostChatSessions.delete(key); + this._extHostChatSessions.delete(URI.revive(sessionResource)); } - async $invokeChatSessionRequestHandler(handle: number, id: string, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise { - const entry = this._extHostChatSessions.get(`${handle}_${id}`); + async $invokeChatSessionRequestHandler(handle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise { + const entry = this._extHostChatSessions.get(URI.revive(sessionResource)); if (!entry || !entry.sessionObj.session.requestHandler) { return {}; } diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 9145bb7d931..d0992ca3851 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -82,7 +82,7 @@ suite('ObservableChatSession', function () { async function createInitializedSession(sessionContent: any, sessionId = 'test-id'): Promise { const resource = URI.parse(`${Schemas.vscodeChatSession}:/test/${sessionId}`); - const session = new ObservableChatSession(sessionId, resource, 1, proxy, logService, dialogService); + const session = new ObservableChatSession(resource, 1, proxy, logService, dialogService); (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); await session.initialize(CancellationToken.None); return session; @@ -91,9 +91,8 @@ suite('ObservableChatSession', function () { test('constructor creates session with proper initial state', function () { const sessionId = 'test-id'; const resource = URI.parse(`${Schemas.vscodeChatSession}:/test/${sessionId}`); - const session = disposables.add(new ObservableChatSession(sessionId, resource, 1, proxy, logService, dialogService)); + const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService)); - assert.strictEqual(session.sessionId, 'test-id'); assert.strictEqual(session.providerHandle, 1); assert.deepStrictEqual(session.history, []); assert.ok(session.progressObs); @@ -107,7 +106,7 @@ suite('ObservableChatSession', function () { test('session queues progress before initialization and processes it after', async function () { const sessionId = 'test-id'; const resource = URI.parse(`${Schemas.vscodeChatSession}:/test/${sessionId}`); - const session = disposables.add(new ObservableChatSession(sessionId, resource, 1, proxy, logService, dialogService)); + const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService)); const progress1: IChatProgress = { kind: 'progressMessage', content: { value: 'Hello', isTrusted: false } }; const progress2: IChatProgress = { kind: 'progressMessage', content: { value: 'World', isTrusted: false } }; @@ -158,7 +157,7 @@ suite('ObservableChatSession', function () { test('initialization is idempotent and returns same promise', async function () { const sessionId = 'test-id'; const resource = URI.parse(`${Schemas.vscodeChatSession}:/test/${sessionId}`); - const session = disposables.add(new ObservableChatSession(sessionId, resource, 1, proxy, logService, dialogService)); + const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService)); const sessionContent = createSessionContent(); (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); @@ -235,7 +234,7 @@ suite('ObservableChatSession', function () { await session.requestHandler!(request, progressCallback, [], CancellationToken.None); - assert.ok((proxy.$invokeChatSessionRequestHandler as sinon.SinonStub).calledOnceWith(1, 'test-id', request, [], CancellationToken.None)); + assert.ok((proxy.$invokeChatSessionRequestHandler as sinon.SinonStubbedMember).calledOnceWith(1, session.sessionResource, request, [], CancellationToken.None)); }); test('request handler forwards progress updates to external callback', async function () { @@ -286,7 +285,7 @@ suite('ObservableChatSession', function () { test('dispose properly cleans up resources and notifies listeners', function () { const sessionId = 'test-id'; const resource = URI.parse(`${Schemas.vscodeChatSession}:/test/${sessionId}`); - const session = disposables.add(new ObservableChatSession(sessionId, resource, 1, proxy, logService, dialogService)); + const session = disposables.add(new ObservableChatSession(resource, 1, proxy, logService, dialogService)); let disposeEventFired = false; const disposable = session.onWillDispose(() => { @@ -296,22 +295,11 @@ suite('ObservableChatSession', function () { session.dispose(); assert.ok(disposeEventFired); - assert.ok((proxy.$disposeChatSessionContent as sinon.SinonStub).calledOnceWith(1, 'test-id')); + assert.ok((proxy.$disposeChatSessionContent as sinon.SinonStubbedMember).calledOnceWith(1, resource)); disposable.dispose(); }); - test('session key generation is consistent', function () { - const sessionId = 'test-id'; - const resource = URI.parse(`${Schemas.vscodeChatSession}:/test/${sessionId}`); - const session = disposables.add(new ObservableChatSession(sessionId, resource, 42, proxy, logService, dialogService)); - - assert.strictEqual(session.sessionKey, '42_test-id'); - assert.strictEqual(ObservableChatSession.generateSessionKey(42, 'test-id'), '42_test-id'); - - session.dispose(); - }); - test('session with multiple request/response pairs in history', async function () { const sessionHistory = [ { type: 'request', prompt: 'First question' }, @@ -436,7 +424,8 @@ suite('MainThreadChatSessions', function () { }); test('provideChatSessionContent creates and initializes session', async function () { - mainThread.$registerChatSessionContentProvider(1, 'test-type'); + const sessionScheme = 'test-session-type'; + mainThread.$registerChatSessionContentProvider(1, sessionScheme); const sessionContent = { id: 'test-session', @@ -445,15 +434,14 @@ suite('MainThreadChatSessions', function () { hasRequestHandler: false }; - const resource = URI.parse(`${Schemas.vscodeChatSession}:/test-type/test-session`); + const resource = URI.parse(`${sessionScheme}:/test-session`); (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); - const session1 = await chatSessionsService.provideChatSessionContent('test-type', 'test-session', resource, CancellationToken.None); + const session1 = await chatSessionsService.provideChatSessionContent(resource, CancellationToken.None); assert.ok(session1); - assert.strictEqual(session1.sessionId, 'test-session'); - const session2 = await chatSessionsService.provideChatSessionContent('test-type', 'test-session', resource, CancellationToken.None); + const session2 = await chatSessionsService.provideChatSessionContent(resource, CancellationToken.None); assert.strictEqual(session1, session2); assert.ok((proxy.$provideChatSessionContent as sinon.SinonStub).calledOnce); @@ -461,7 +449,9 @@ suite('MainThreadChatSessions', function () { }); test('$handleProgressChunk routes to correct session', async function () { - mainThread.$registerChatSessionContentProvider(1, 'test-type'); + const sessionScheme = 'test-session-type'; + + mainThread.$registerChatSessionContentProvider(1, sessionScheme); const sessionContent = { id: 'test-session', @@ -472,11 +462,11 @@ suite('MainThreadChatSessions', function () { (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); - const resource = URI.parse(`${Schemas.vscodeChatSession}:/test-type/test-session`); - const session = await chatSessionsService.provideChatSessionContent('test-type', 'test-session', resource, CancellationToken.None) as ObservableChatSession; + const resource = URI.parse(`${sessionScheme}:/test-session`); + const session = await chatSessionsService.provideChatSessionContent(resource, CancellationToken.None) as ObservableChatSession; const progressDto: IChatProgressDto = { kind: 'progressMessage', content: { value: 'Test', isTrusted: false } }; - await mainThread.$handleProgressChunk(1, 'test-session', 'req1', [progressDto]); + await mainThread.$handleProgressChunk(1, resource, 'req1', [progressDto]); assert.strictEqual(session.progressObs.get().length, 1); assert.strictEqual(session.progressObs.get()[0].kind, 'progressMessage'); @@ -485,7 +475,8 @@ suite('MainThreadChatSessions', function () { }); test('$handleProgressComplete marks session complete', async function () { - mainThread.$registerChatSessionContentProvider(1, 'test-type'); + const sessionScheme = 'test-session-type'; + mainThread.$registerChatSessionContentProvider(1, sessionScheme); const sessionContent = { id: 'test-session', @@ -496,12 +487,12 @@ suite('MainThreadChatSessions', function () { (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); - const resource = URI.parse(`${Schemas.vscodeChatSession}:/test-type/test-session`); - const session = await chatSessionsService.provideChatSessionContent('test-type', 'test-session', resource, CancellationToken.None) as ObservableChatSession; + const resource = URI.parse(`${sessionScheme}:/test-session`); + const session = await chatSessionsService.provideChatSessionContent(resource, CancellationToken.None) as ObservableChatSession; const progressDto: IChatProgressDto = { kind: 'progressMessage', content: { value: 'Test', isTrusted: false } }; - await mainThread.$handleProgressChunk(1, 'test-session', 'req1', [progressDto]); - mainThread.$handleProgressComplete(1, 'test-session', 'req1'); + await mainThread.$handleProgressChunk(1, resource, 'req1', [progressDto]); + mainThread.$handleProgressComplete(1, resource, 'req1'); assert.strictEqual(session.isCompleteObs.get(), true); @@ -509,7 +500,8 @@ suite('MainThreadChatSessions', function () { }); test('integration with multiple request/response pairs', async function () { - mainThread.$registerChatSessionContentProvider(1, 'test-type'); + const sessionScheme = 'test-session-type'; + mainThread.$registerChatSessionContentProvider(1, sessionScheme); const sessionContent = { id: 'multi-turn-session', @@ -525,12 +517,11 @@ suite('MainThreadChatSessions', function () { (proxy.$provideChatSessionContent as sinon.SinonStub).resolves(sessionContent); - const resource = URI.parse(`${Schemas.vscodeChatSession}:/test-type/multi-turn-session`); - const session = await chatSessionsService.provideChatSessionContent('test-type', 'multi-turn-session', resource, CancellationToken.None) as ObservableChatSession; + const resource = URI.parse(`${sessionScheme}:/multi-turn-session`); + const session = await chatSessionsService.provideChatSessionContent(resource, CancellationToken.None) as ObservableChatSession; // Verify the session loaded correctly assert.ok(session); - assert.strictEqual(session.sessionId, 'multi-turn-session'); assert.strictEqual(session.history.length, 4); // Verify all history items are correctly loaded diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts index 796beb79aee..db4d47ca0f5 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts @@ -75,7 +75,7 @@ export class RenameChatSessionAction extends Action2 { try { // Find the chat sessions view and trigger inline rename mode // This is similar to how file renaming works in the explorer - await chatSessionsService.setEditableSession(sessionId, { + await chatSessionsService.setEditableSession(context.session.resource, { validationMessage: (value: string) => { if (!value || value.trim().length === 0) { return { content: localize('renameSession.emptyName', "Name cannot be empty"), severity: Severity.Error }; @@ -101,7 +101,7 @@ export class RenameChatSessionAction extends Action2 { ); } } - await chatSessionsService.setEditableSession(sessionId, null); + await chatSessionsService.setEditableSession(context.session.resource, null); } }); } catch (error) { diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 226ec95258d..64ad4ad4632 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -6,7 +6,7 @@ import { timeout } from '../../../../base/common/async.js'; import { Event } from '../../../../base/common/event.js'; import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { PolicyCategory } from '../../../../base/common/policy.js'; @@ -41,6 +41,7 @@ import { ChatModeService, IChatModeService } from '../common/chatModes.js'; import { ChatResponseResourceFileSystemProvider } from '../common/chatResponseResourceFileSystemProvider.js'; import { IChatService } from '../common/chatService.js'; import { ChatService } from '../common/chatServiceImpl.js'; +import { IChatSessionsService } from '../common/chatSessionsService.js'; import { ChatSlashCommandService, IChatSlashCommandService } from '../common/chatSlashCommands.js'; import { ChatTodoListService, IChatTodoListService } from '../common/chatTodoListService.js'; import { ChatTransferService, IChatTransferService } from '../common/chatTransferService.js'; @@ -82,6 +83,7 @@ import { ChatSessionsGettingStartedAction, DeleteChatSessionAction, OpenChatSess import { registerChatTitleActions } from './actions/chatTitleActions.js'; import { registerChatToolActions } from './actions/chatToolActions.js'; import { ChatTransferContribution } from './actions/chatTransfer.js'; +import './agentSessions/agentSessionsView.js'; import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from './chat.js'; import { ChatAccessibilityService } from './chatAccessibilityService.js'; import './chatAttachmentModel.js'; @@ -120,7 +122,6 @@ import { ChatRelatedFilesContribution } from './contrib/chatInputRelatedFilesCon import { LanguageModelToolsService, globalAutoApproveDescription } from './languageModelToolsService.js'; import './promptSyntax/promptCodingAgentActionContribution.js'; import './promptSyntax/promptToolsCodeLensProvider.js'; -import './agentSessions/agentSessionsView.js'; import { PromptUrlHandler } from './promptSyntax/promptUrlHandler.js'; import { SAVE_TO_PROMPT_ACTION_ID, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './promptSyntax/saveToPromptAction.js'; import { ConfigureToolSets, UserToolSetsContributions } from './tools/toolSetsContribution.js'; @@ -762,14 +763,34 @@ class ChatResolverContribution extends Disposable { static readonly ID = 'workbench.contrib.chatResolver'; + private readonly _editorRegistrations = this._register(new DisposableMap()); + constructor( - @IEditorResolverService editorResolverService: IEditorResolverService, - @IInstantiationService instantiationService: IInstantiationService, + @IChatSessionsService chatSessionsService: IChatSessionsService, + @IEditorResolverService private readonly editorResolverService: IEditorResolverService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); - this._register(editorResolverService.registerEditor( - `{${Schemas.vscodeChatEditor},${Schemas.vscodeChatSession}}:**/**`, + this._registerEditor(Schemas.vscodeChatEditor); + this._registerEditor(Schemas.vscodeChatSession); + + this._register(chatSessionsService.onDidChangeContentProviderSchemes((e) => { + for (const scheme of e.added) { + this._registerEditor(scheme); + } + for (const scheme of e.removed) { + this._editorRegistrations.deleteAndDispose(scheme); + } + })); + + for (const scheme of chatSessionsService.getContentProviderSchemes()) { + this._registerEditor(scheme); + } + } + + private _registerEditor(scheme: string): void { + this._editorRegistrations.set(scheme, this.editorResolverService.registerEditor(`${scheme}:**/**`, { id: ChatEditorInput.EditorID, label: nls.localize('chat', "Chat"), @@ -777,11 +798,14 @@ class ChatResolverContribution extends Disposable { }, { singlePerResource: true, - canSupportResource: resource => resource.scheme === Schemas.vscodeChatEditor || resource.scheme === Schemas.vscodeChatSession, + canSupportResource: resource => resource.scheme === scheme, }, { createEditorInput: ({ resource, options }) => { - return { editor: instantiationService.createInstance(ChatEditorInput, resource, options as IChatEditorOptions), options }; + return { + editor: this.instantiationService.createInstance(ChatEditorInput, resource, options as IChatEditorOptions), + options + }; } } )); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index e353947423a..576b2fabec1 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -200,7 +200,7 @@ export class ChatEditor extends EditorPane { if (chatSessionType !== 'local') { try { - await raceCancellationError(this.chatSessionsService.canResolveContentProvider(chatSessionType), token); + await raceCancellationError(this.chatSessionsService.canResolveChatSession(input.resource), token); const contributions = this.chatSessionsService.getAllChatSessionContributions(); const contribution = contributions.find(c => c.type === chatSessionType); if (contribution) { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index ca272425830..1ce03b15450 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -73,8 +73,6 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler if (!parsed || typeof parsed !== 'number') { throw new Error('Invalid chat URI'); } - } else if (resource.scheme !== Schemas.vscodeChatSession) { - throw new Error('Invalid chat URI'); } this.sessionId = (options.target && 'sessionId' in options.target) ? @@ -269,7 +267,7 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler const searchParams = new URLSearchParams(this.resource.query); const chatSessionType = searchParams.get('chatSessionType'); const inputType = chatSessionType ?? this.resource.authority; - if (this.resource.scheme === Schemas.vscodeChatSession) { + if (this.resource.scheme !== Schemas.vscodeChatEditor) { this.model = await this.chatService.loadSessionForResource(this.resource, ChatAgentLocation.Chat, CancellationToken.None); } else if (typeof this.sessionId === 'string') { this.model = await this.chatService.getOrRestoreSession(this.sessionId) diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index e42bfe32ee5..1d41e1380cb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -662,7 +662,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (!ctx) { continue; } - if (!this.chatSessionsService.getSessionOption(ctx.chatSessionType, ctx.chatSessionId, optionGroup.id)) { + if (!this.chatSessionsService.getSessionOption(ctx.chatSessionType, ctx.chatSessionResource, optionGroup.id)) { // This session does not have a value to contribute for this option group continue; } @@ -682,7 +682,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.getOrCreateOptionEmitter(optionGroup.id).fire(option); this.chatSessionsService.notifySessionOptionsChange( ctx.chatSessionType, - ctx.chatSessionId, + ctx.chatSessionResource, [{ optionId: optionGroup.id, value: option.id }] ).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); }, @@ -1139,7 +1139,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return hideAll(); } - if (!this.chatSessionsService.hasAnySessionOptions(ctx.chatSessionType, ctx.chatSessionId)) { + if (!this.chatSessionsService.hasAnySessionOptions(ctx.chatSessionResource)) { return hideAll(); } @@ -1167,7 +1167,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } for (const [optionGroupId] of this.chatSessionPickerWidgets.entries()) { - const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionType, ctx.chatSessionId, optionGroupId); + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionType, ctx.chatSessionResource, optionGroupId); if (currentOption) { const optionGroup = optionGroups.find(g => g.id === optionGroupId); if (optionGroup) { @@ -1212,7 +1212,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - const currentOptionId = this.chatSessionsService.getSessionOption(ctx.chatSessionType, ctx.chatSessionId, optionGroupId); + const currentOptionId = this.chatSessionsService.getSessionOption(ctx.chatSessionType, ctx.chatSessionResource, optionGroupId); return optionGroup.items.find(m => m.id === currentOptionId); } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index bab7bd993db..8b78f1b82b8 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -7,6 +7,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; @@ -25,7 +26,6 @@ import { ChatEditorInput } from '../browser/chatEditorInput.js'; import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentRequest, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatSession, ChatSessionStatus, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionsExtensionPoint, IChatSessionsService } from '../common/chatSessionsService.js'; -import { ChatSessionUri } from '../common/chatUri.js'; import { AGENT_SESSIONS_VIEWLET_ID, ChatAgentLocation, ChatModeKind } from '../common/constants.js'; import { CHAT_CATEGORY } from './actions/chatActions.js'; import { IChatEditorOptions } from './chatEditor.js'; @@ -184,9 +184,9 @@ class ContributedChatSessionData implements IDisposable { constructor( readonly session: ChatSession, readonly chatSessionType: string, - readonly id: string, + readonly resource: URI, readonly options: Record | undefined, - private readonly onWillDispose: (session: ChatSession, chatSessionType: string, id: string) => void + private readonly onWillDispose: (resource: URI) => void ) { this._optionsCache = new Map(); if (options) { @@ -196,7 +196,7 @@ class ContributedChatSessionData implements IDisposable { } this._disposableStore = new DisposableStore(); this._disposableStore.add(this.session.onWillDispose(() => { - this.onWillDispose(this.session, this.chatSessionType, this.id); + this.onWillDispose(this.resource); })); } @@ -208,21 +208,29 @@ class ContributedChatSessionData implements IDisposable { export class ChatSessionsService extends Disposable implements IChatSessionsService { readonly _serviceBrand: undefined; - private readonly _itemsProviders: Map = new Map(); - private readonly _onDidChangeItemsProviders = this._register(new Emitter()); - readonly onDidChangeItemsProviders: Event = this._onDidChangeItemsProviders.event; - private readonly _contentProviders: Map = new Map(); - private readonly _contributions: Map = new Map(); + private readonly _itemsProviders: Map = new Map(); + private readonly _contentProviders: Map = new Map(); + private readonly _contributions: Map = new Map(); private readonly _alternativeIdMap: Map = new Map(); private readonly _disposableStores: Map = new Map(); private readonly _contextKeys = new Set(); + + private readonly _onDidChangeItemsProviders = this._register(new Emitter()); + readonly onDidChangeItemsProviders: Event = this._onDidChangeItemsProviders.event; + private readonly _onDidChangeSessionItems = this._register(new Emitter()); readonly onDidChangeSessionItems: Event = this._onDidChangeSessionItems.event; + private readonly _onDidChangeAvailability = this._register(new Emitter()); readonly onDidChangeAvailability: Event = this._onDidChangeAvailability.event; + private readonly _onDidChangeInProgress = this._register(new Emitter()); public get onDidChangeInProgress() { return this._onDidChangeInProgress.event; } + + private readonly _onDidChangeContentProviderSchemes = this._register(new Emitter<{ readonly added: string[]; readonly removed: string[] }>()); + public get onDidChangeContentProviderSchemes() { return this._onDidChangeContentProviderSchemes.event; } + private readonly inProgressMap: Map = new Map(); private readonly _sessionTypeOptions: Map = new Map(); private readonly _sessionTypeIcons: Map = new Map(); @@ -231,6 +239,9 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private readonly _sessionTypeWelcomeTips: Map = new Map(); private readonly _sessionTypeInputPlaceholders: Map = new Map(); + private readonly _sessions = new ResourceMap(); + private readonly _editableSessions = new ResourceMap(); + constructor( @ILogService private readonly _logService: ILogService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, @@ -503,11 +514,11 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ fallback: localize('chatEditorContributionName', "{0}", contribution.displayName), } }; - const untitledId = `untitled-${generateUuid()}`; - await editorService.openEditor({ - resource: ChatSessionUri.forSession(type, untitledId), - options, + const resource = URI.from({ + scheme: type, + path: `/untitled-${generateUuid()}`, }); + await editorService.openEditor({ resource, options }); } catch (e) { logService.error(`Failed to open new '${type}' chat session editor`, e); } @@ -558,10 +569,10 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private _disposeSessionsForContribution(contributionId: string): void { // Find and dispose all sessions that belong to this contribution - const sessionsToDispose: string[] = []; - for (const [sessionKey, sessionData] of this._sessions) { + const sessionsToDispose: URI[] = []; + for (const [sessionResource, sessionData] of this._sessions) { if (sessionData.chatSessionType === contributionId) { - sessionsToDispose.push(sessionKey); + sessionsToDispose.push(sessionResource); } } @@ -641,25 +652,20 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._itemsProviders.has(chatViewType); } - async canResolveContentProvider(chatViewType: string) { + async canResolveChatSession(chatSessionResource: URI) { await this._extensionService.whenInstalledExtensionsRegistered(); - const resolvedType = this._resolveToPrimaryType(chatViewType); - if (resolvedType) { - chatViewType = resolvedType; - } - - const contribution = this._contributions.get(chatViewType); + const resolvedType = this._resolveToPrimaryType(chatSessionResource.scheme) || chatSessionResource.scheme; + const contribution = this._contributions.get(resolvedType); if (contribution && !this._isContributionAvailable(contribution)) { return false; } - if (this._contentProviders.has(chatViewType)) { + if (this._contentProviders.has(chatSessionResource.scheme)) { return true; } - await this._extensionService.activateByEvent(`onChatSession:${chatViewType}`); - - return this._contentProviders.has(chatViewType); + await this._extensionService.activateByEvent(`onChatSession:${chatSessionResource.scheme}`); + return this._contentProviders.has(chatSessionResource.scheme); } public async provideChatSessionItems(chatSessionType: string, token: CancellationToken): Promise { @@ -709,11 +715,19 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable { + if (this._contentProviders.has(chatSessionType)) { + throw new Error(`Content provider for ${chatSessionType} is already registered.`); + } + this._contentProviders.set(chatSessionType, provider); + this._onDidChangeContentProviderSchemes.fire({ added: [chatSessionType], removed: [] }); + return { dispose: () => { this._contentProviders.delete(chatSessionType); + this._onDidChangeContentProviderSchemes.fire({ added: [], removed: [chatSessionType] }); + // Remove all sessions that were created by this provider for (const [key, session] of this._sessions) { if (session.chatSessionType === chatSessionType) { @@ -725,11 +739,6 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ }; } - private readonly _sessions = new Map(); - - // Editable session support - private readonly _editableSessions = new Map(); - /** * Creates a new chat session by delegating to the appropriate provider * @param chatSessionType The type of chat session provider to use @@ -760,77 +769,65 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return chatSessionItem; } - public async provideChatSessionContent(chatSessionType: string, id: string, resource: URI, token: CancellationToken): Promise { - if (!(await this.canResolveContentProvider(chatSessionType))) { - throw Error(`Can not find provider for ${chatSessionType}`); + public async provideChatSessionContent(sessionResource: URI, token: CancellationToken): Promise { + if (!(await this.canResolveChatSession(sessionResource))) { + throw Error(`Can not find provider for ${sessionResource}`); } - - const resolvedType = this._resolveToPrimaryType(chatSessionType); - if (resolvedType) { - chatSessionType = resolvedType; - } - - - const provider = this._contentProviders.get(chatSessionType); + const resolvedType = this._resolveToPrimaryType(sessionResource.scheme) || sessionResource.scheme; + const provider = this._contentProviders.get(resolvedType); if (!provider) { - throw Error(`Can not find provider for ${chatSessionType}`); + throw Error(`Can not find provider for ${sessionResource}`); } - const sessionKey = `${chatSessionType}_${id}`; - const existingSessionData = this._sessions.get(sessionKey); + const existingSessionData = this._sessions.get(sessionResource); if (existingSessionData) { return existingSessionData.session; } - const session = await provider.provideChatSessionContent(id, resource, token); - const sessionData = new ContributedChatSessionData(session, chatSessionType, id, session.options, this._onWillDisposeSession.bind(this)); + const session = await provider.provideChatSessionContent(sessionResource, token); + const sessionData = new ContributedChatSessionData(session, sessionResource.scheme, sessionResource, session.options, this._onWillDisposeSession.bind(this)); - this._sessions.set(sessionKey, sessionData); + this._sessions.set(sessionResource, sessionData); return session; } - private _onWillDisposeSession(session: ChatSession, chatSessionType: string, id: string): void { - const sessionKey = `${chatSessionType}_${id}`; - this._sessions.delete(sessionKey); + private _onWillDisposeSession(sessionResource: URI): void { + this._sessions.delete(sessionResource); } - public hasAnySessionOptions(chatSessionType: string, id: string): boolean { - const sessionKey = `${chatSessionType}_${id}`; - const session = this._sessions.get(sessionKey); + public hasAnySessionOptions(resource: URI): boolean { + const session = this._sessions.get(resource); return !!session && !!session.options && Object.keys(session.options).length > 0; } - - public getSessionOption(chatSessionType: string, id: string, optionId: string): string | undefined { - const sessionKey = `${chatSessionType}_${id}`; - const session = this._sessions.get(sessionKey); + public getSessionOption(chatSessionType: string, resource: URI, optionId: string): string | undefined { + const session = this._sessions.get(resource); return session?.getOption(optionId); } - public setSessionOption(chatSessionType: string, id: string, optionId: string, value: string): boolean { - const sessionKey = `${chatSessionType}_${id}`; - const session = this._sessions.get(sessionKey); + public setSessionOption(chatSessionType: string, resource: URI, optionId: string, value: string): boolean { + const session = this._sessions.get(resource); return !!session?.setOption(optionId, value); } // Implementation of editable session methods - public async setEditableSession(sessionId: string, data: IEditableData | null): Promise { + public async setEditableSession(sessionResource: URI, data: IEditableData | null): Promise { if (!data) { - this._editableSessions.delete(sessionId); + this._editableSessions.delete(sessionResource); } else { - this._editableSessions.set(sessionId, data); + this._editableSessions.set(sessionResource, data); } // Trigger refresh of the session views that might need to update their rendering this._onDidChangeSessionItems.fire('local'); } - public getEditableData(sessionId: string): IEditableData | undefined { - return this._editableSessions.get(sessionId); + public getEditableData(sessionResource: URI): IEditableData | undefined { + return this._editableSessions.get(sessionResource); } - public isEditable(sessionId: string): boolean { - return this._editableSessions.has(sessionId); + public isEditable(sessionResource: URI): boolean { + return this._editableSessions.has(sessionResource); } public notifySessionItemsChanged(chatSessionType: string): void { @@ -855,27 +852,27 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._sessionTypeOptions.get(chatSessionType); } - private _optionsChangeCallback?: (chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise; + private _optionsChangeCallback?: (chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise; /** * Set the callback for notifying extensions about option changes */ - public setOptionsChangeCallback(callback: (chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise): void { + public setOptionsChangeCallback(callback: (chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise): void { this._optionsChangeCallback = callback; } /** * Notify extension about option changes for a session */ - public async notifySessionOptionsChange(chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { + public async notifySessionOptionsChange(chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { if (!updates.length) { return; } if (this._optionsChangeCallback) { - await this._optionsChangeCallback(chatSessionType, sessionId, updates); + await this._optionsChangeCallback(chatSessionType, sessionResource, updates); } for (const u of updates) { - this.setSessionOption(chatSessionType, sessionId, u.optionId, u.value); + this.setSessionOption(chatSessionType, sessionResource, u.optionId, u.value); } } @@ -921,6 +918,10 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public getWelcomeTipsForSessionType(chatSessionType: string): string | undefined { return this._sessionTypeWelcomeTips.get(chatSessionType); } + + public getContentProviderSchemes(): string[] { + return Array.from(this._contentProviders.keys()); + } } registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts index 801aa12a3b7..0e112637713 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/common.ts @@ -11,7 +11,6 @@ import { IEditorGroupsService } from '../../../../services/editor/common/editorG import { ChatContextKeys } from '../../common/chatContextKeys.js'; import { IChatService } from '../../common/chatService.js'; import { IChatSessionItem, IChatSessionItemProvider } from '../../common/chatSessionsService.js'; -import { ChatSessionUri } from '../../common/chatUri.js'; import { IChatWidgetService } from '../chat.js'; import { ChatEditorInput } from '../chatEditorInput.js'; @@ -49,26 +48,11 @@ export function isChatSession(editor?: EditorInput): boolean { * Returns chat session type from a URI, or 'local' if not specified or cannot be determined. */ export function getChatSessionType(editor: ChatEditorInput): string { - if (!editor.resource) { + if (editor.resource.scheme === Schemas.vscodeChatEditor || editor.resource.scheme === Schemas.vscodeChatSession) { return 'local'; } - const { scheme, query } = editor.resource; - - if (scheme === Schemas.vscodeChatSession) { - const parsed = ChatSessionUri.parse(editor.resource); - if (parsed) { - return parsed.chatSessionType; - } - } - - const sessionTypeFromQuery = new URLSearchParams(query).get('chatSessionType'); - if (sessionTypeFromQuery) { - return sessionTypeFromQuery; - } - - // Default to 'local' for vscode-chat-editor scheme or when type cannot be determined - return 'local'; + return editor.resource.scheme; } /** diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts index 3ea4e046fd5..2b80c437426 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts @@ -200,7 +200,7 @@ export class SessionsRenderer extends Disposable implements ITreeRenderer c.type === parsed.chatSessionType); if (contribution) { diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 34cd1279c58..de81e4ccbf6 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -11,7 +11,7 @@ import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { autorun, autorunSelfDisposable, IObservable, IReader } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { URI } from '../../../../base/common/uri.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; import { ISelection } from '../../../../editor/common/core/selection.js'; import { Command, Location, TextEdit } from '../../../../editor/common/languages.js'; @@ -938,7 +938,7 @@ export interface IChatService { export interface IChatSessionContext { chatSessionType: string; chatSessionId: string; - chatSessionResource: UriComponents; + chatSessionResource: URI; isUntitled: boolean; } diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a17248e0a27..284739f7bea 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -11,6 +11,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; import { revive } from '../../../../base/common/marshalling.js'; import { autorun, derived, IObservable, ObservableMap } from '../../../../base/common/observable.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; @@ -36,7 +37,6 @@ import { IChatSessionsService } from './chatSessionsService.js'; import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatTransferService } from './chatTransferService.js'; -import { ChatSessionUri } from './chatUri.js'; import { IChatRequestVariableEntry } from './chatVariableEntries.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js'; import { ChatMessageRole, IChatMessage } from './languageModels.js'; @@ -72,7 +72,7 @@ export class ChatService extends Disposable implements IChatService { declare _serviceBrand: undefined; private readonly _sessionModels = new ObservableMap(); - private readonly _contentProviderSessionModels = new Map>(); + private readonly _contentProviderSessionModels = new Map>(); private readonly _pendingRequests = this._register(new DisposableMap()); private _persistedSessions: ISerializableChatsData; @@ -81,8 +81,8 @@ export class ChatService extends Disposable implements IChatService { return this._transferredSessionData; } - private readonly _onDidSubmitRequest = this._register(new Emitter<{ chatSessionId: string }>()); - public readonly onDidSubmitRequest: Event<{ chatSessionId: string }> = this._onDidSubmitRequest.event; + private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionId: string }>()); + public readonly onDidSubmitRequest = this._onDidSubmitRequest.event; private readonly _onDidPerformUserAction = this._register(new Emitter()); public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; @@ -452,39 +452,31 @@ export class ChatService extends Disposable implements IChatService { async loadSessionForResource(chatSessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { // TODO: Move this into a new ChatModelService - const parsed = ChatSessionUri.parse(chatSessionResource); - if (!parsed) { - throw new Error('Invalid chat session URI'); - } - const existing = this._contentProviderSessionModels.get(parsed.chatSessionType)?.get(parsed.sessionId); + const existing = this._contentProviderSessionModels.get(chatSessionResource.scheme)?.get(chatSessionResource); if (existing) { return existing.model; } - if (parsed.chatSessionType === 'local') { - return this.getOrRestoreSession(parsed.sessionId); - } - - const chatSessionType = parsed.chatSessionType; - const content = await this.chatSessionService.provideChatSessionContent(chatSessionType, parsed.sessionId, chatSessionResource, CancellationToken.None); + const content = await this.chatSessionService.provideChatSessionContent(chatSessionResource, CancellationToken.None); + const chatSessionType = chatSessionResource.scheme; // Contributed sessions do not use UI tools const model = this._startSession(undefined, location, true, CancellationToken.None, { canUseTools: false, inputType: chatSessionType }); model.setContributedChatSession({ - chatSessionType, - chatSessionId: parsed.sessionId, + chatSessionType: chatSessionType, + chatSessionId: chatSessionResource.toString(), chatSessionResource, - isUntitled: parsed.sessionId.startsWith('untitled-') //TODO(jospicer) + isUntitled: chatSessionResource.path.startsWith('/untitled-') //TODO(jospicer) }); if (!this._contentProviderSessionModels.has(chatSessionType)) { - this._contentProviderSessionModels.set(chatSessionType, new Map()); + this._contentProviderSessionModels.set(chatSessionType, new ResourceMap()); } const disposables = new DisposableStore(); - this._contentProviderSessionModels.get(chatSessionType)!.set(parsed.sessionId, { model, disposables }); + this._contentProviderSessionModels.get(chatSessionType)!.set(chatSessionResource, { model, disposables }); disposables.add(model.onDidDispose(() => { - this._contentProviderSessionModels?.get(chatSessionType)?.delete(parsed.sessionId); + this._contentProviderSessionModels?.get(chatSessionType)?.delete(chatSessionResource); content.dispose(); })); diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index e87eda82afe..faf3836136c 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -91,7 +91,6 @@ export type IChatSessionHistoryItem = { }; export interface ChatSession extends IDisposable { - readonly sessionId: string; readonly sessionResource: URI; readonly onWillDispose: Event; history: Array; @@ -124,7 +123,7 @@ export interface IChatSessionItemProvider { } export interface IChatSessionContentProvider { - provideChatSessionContent(sessionId: string, sessionResource: URI, token: CancellationToken): Promise; + provideChatSessionContent(sessionResource: URI, token: CancellationToken): Promise; } export interface IChatSessionsService { @@ -159,10 +158,6 @@ export interface IChatSessionsService { reportInProgress(chatSessionType: string, count: number): void; getInProgress(): { displayName: string; count: number }[]; - registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable; - canResolveContentProvider(chatSessionType: string): Promise; - provideChatSessionContent(chatSessionType: string, id: string, sessionResource: URI, token: CancellationToken): Promise; - // Get available option groups for a session type getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; @@ -170,27 +165,40 @@ export interface IChatSessionsService { setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; // Set callback for notifying extensions about option changes - setOptionsChangeCallback(callback: (chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise): void; + setOptionsChangeCallback(callback: (chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise): void; // Notify extension about option changes - notifySessionOptionsChange(chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise; + notifySessionOptionsChange(chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise; // Editable session support - setEditableSession(sessionId: string, data: IEditableData | null): Promise; - getEditableData(sessionId: string): IEditableData | undefined; - isEditable(sessionId: string): boolean; + setEditableSession(sessionResource: URI, data: IEditableData | null): Promise; + getEditableData(sessionResource: URI): IEditableData | undefined; + isEditable(sessionResource: URI): boolean; // Notify providers about session items changes notifySessionItemsChanged(chatSessionType: string): void; - hasAnySessionOptions(chatSessionType: string, sessionId: string): boolean; - getSessionOption(chatSessionType: string, sessionId: string, optionId: string): string | undefined; - setSessionOption(chatSessionType: string, sessionId: string, optionId: string, value: string): boolean; + // #region Content provider support + + // TODO: Split into separate service? + readonly onDidChangeContentProviderSchemes: Event<{ readonly added: string[]; readonly removed: string[] }>; + + getContentProviderSchemes(): string[]; + + registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable; + canResolveChatSession(chatSessionResource: URI): Promise; + provideChatSessionContent(sessionResource: URI, token: CancellationToken): Promise; + + hasAnySessionOptions(resource: URI): boolean; + getSessionOption(chatSessionType: string, sessionResource: URI, optionId: string): string | undefined; + setSessionOption(chatSessionType: string, sessionResource: URI, optionId: string, value: string): boolean; /** * Get the capabilities for a specific session type */ getCapabilitiesForSessionType(chatSessionType: string): IChatAgentAttachmentCapabilities | undefined; + + // #endregion } export const IChatSessionsService = createDecorator('chatSessionsService'); diff --git a/src/vs/workbench/contrib/chat/common/chatUri.ts b/src/vs/workbench/contrib/chat/common/chatUri.ts index 132303190ff..c945748770e 100644 --- a/src/vs/workbench/contrib/chat/common/chatUri.ts +++ b/src/vs/workbench/contrib/chat/common/chatUri.ts @@ -12,6 +12,9 @@ export type ChatSessionIdentifier = { readonly sessionId: string; }; +/** + * @deprecated + */ export namespace ChatSessionUri { export const scheme = Schemas.vscodeChatSession; diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 3c03129bec6..a497c9cc0a7 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -6,11 +6,12 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Emitter } from '../../../../../base/common/event.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IEditableData } from '../../../../common/views.js'; import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/chatAgents.js'; import { ChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; -import { IEditableData } from '../../../../common/views.js'; export class MockChatSessionsService implements IChatSessionsService { _serviceBrand: undefined; @@ -27,12 +28,15 @@ export class MockChatSessionsService implements IChatSessionsService { private readonly _onDidChangeInProgress = new Emitter(); readonly onDidChangeInProgress = this._onDidChangeInProgress.event; + private readonly _onDidChangeContentProviderSchemes = new Emitter<{ readonly added: string[]; readonly removed: string[] }>(); + readonly onDidChangeContentProviderSchemes = this._onDidChangeContentProviderSchemes.event; + private providers = new Map(); private contentProviders = new Map(); private contributions: IChatSessionsExtensionPoint[] = []; private optionGroups = new Map(); - private sessionOptions = new Map>(); - private editableData = new Map(); + private sessionOptions = new ResourceMap>(); + private editableData = new ResourceMap(); private inProgress = new Map(); // For testing: allow triggering events @@ -125,6 +129,7 @@ export class MockChatSessionsService implements IChatSessionsService { registerChatSessionContentProvider(chatSessionType: string, provider: IChatSessionContentProvider): IDisposable { this.contentProviders.set(chatSessionType, provider); + this._onDidChangeContentProviderSchemes.fire({ added: [chatSessionType], removed: [] }); return { dispose: () => { this.contentProviders.delete(chatSessionType); @@ -136,12 +141,16 @@ export class MockChatSessionsService implements IChatSessionsService { return this.contentProviders.has(chatSessionType); } - async provideChatSessionContent(chatSessionType: string, id: string, sessionResource: URI, token: CancellationToken): Promise { - const provider = this.contentProviders.get(chatSessionType); + async provideChatSessionContent(sessionResource: URI, token: CancellationToken): Promise { + const provider = this.contentProviders.get(sessionResource.scheme); if (!provider) { - throw new Error(`No content provider for ${chatSessionType}`); + throw new Error(`No content provider for ${sessionResource.scheme}`); } - return provider.provideChatSessionContent(id, sessionResource, token); + return provider.provideChatSessionContent(sessionResource, token); + } + + async canResolveChatSession(chatSessionResource: URI): Promise { + return this.contentProviders.has(chatSessionResource.scheme); } getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined { @@ -156,59 +165,59 @@ export class MockChatSessionsService implements IChatSessionsService { } } - private optionsChangeCallback?: (chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise; + private optionsChangeCallback?: (chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise; - setOptionsChangeCallback(callback: (chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise): void { + setOptionsChangeCallback(callback: (chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => Promise): void { this.optionsChangeCallback = callback; } - async notifySessionOptionsChange(chatSessionType: string, sessionId: string, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { + async notifySessionOptionsChange(chatSessionType: string, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { if (this.optionsChangeCallback) { - await this.optionsChangeCallback(chatSessionType, sessionId, updates); + await this.optionsChangeCallback(chatSessionType, sessionResource, updates); } } - async setEditableSession(sessionId: string, data: IEditableData | null): Promise { + async setEditableSession(sessionResource: URI, data: IEditableData | null): Promise { if (data) { - this.editableData.set(sessionId, data); + this.editableData.set(sessionResource, data); } else { - this.editableData.delete(sessionId); + this.editableData.delete(sessionResource); } } - getEditableData(sessionId: string): IEditableData | undefined { - return this.editableData.get(sessionId); + getEditableData(sessionResource: URI): IEditableData | undefined { + return this.editableData.get(sessionResource); } - isEditable(sessionId: string): boolean { - return this.editableData.has(sessionId); + isEditable(sessionResource: URI): boolean { + return this.editableData.has(sessionResource); } notifySessionItemsChanged(chatSessionType: string): void { this._onDidChangeSessionItems.fire(chatSessionType); } - getSessionOption(chatSessionType: string, sessionId: string, optionId: string): string | undefined { - const sessionKey = `${chatSessionType}:${sessionId}`; - return this.sessionOptions.get(sessionKey)?.get(optionId); + getSessionOption(chatSessionType: string, sessionResource: URI, optionId: string): string | undefined { + return this.sessionOptions.get(sessionResource)?.get(optionId); } - setSessionOption(chatSessionType: string, sessionId: string, optionId: string, value: string): boolean { - const sessionKey = `${chatSessionType}:${sessionId}`; - if (!this.sessionOptions.has(sessionKey)) { - this.sessionOptions.set(sessionKey, new Map()); + setSessionOption(chatSessionType: string, sessionResource: URI, optionId: string, value: string): boolean { + if (!this.sessionOptions.has(sessionResource)) { + this.sessionOptions.set(sessionResource, new Map()); } - this.sessionOptions.get(sessionKey)!.set(optionId, value); + this.sessionOptions.get(sessionResource)!.set(optionId, value); return true; } - hasAnySessionOptions(chatSessionType: string, sessionId: string): boolean { - const sessionKey = `${chatSessionType}:${sessionId}`; - const options = this.sessionOptions.get(sessionKey); - return options !== undefined && options.size > 0; + hasAnySessionOptions(resource: URI): boolean { + return this.sessionOptions.has(resource) && this.sessionOptions.get(resource)!.size > 0; } getCapabilitiesForSessionType(chatSessionType: string): IChatAgentAttachmentCapabilities | undefined { return this.contributions.find(c => c.type === chatSessionType)?.capabilities; } + + getContentProviderSchemes(): string[] { + return Array.from(this.contentProviders.keys()); + } } diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index e40a444b9c4..3e7c795fbe5 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 2 +// version: 3 declare module 'vscode' { /** @@ -35,6 +35,14 @@ declare module 'vscode' { */ readonly onDidChangeChatSessionItems: Event; + /** + * Provides a list of chat sessions. + */ + // TODO: Do we need a flag to try auth if needed? + provideChatSessionItems(token: CancellationToken): ProviderResult; + + // #region Unstable parts of API + /** * Event that the provider can fire to signal that the current (original) chat session should be replaced with a new (modified) chat session. * The UI can use this information to gracefully migrate the user to the new session. @@ -61,27 +69,16 @@ declare module 'vscode' { metadata?: any; }, token: CancellationToken): ProviderResult; - /** - * Provides a list of chat sessions. - */ - // TODO: Do we need a flag to try auth if needed? - provideChatSessionItems(token: CancellationToken): ProviderResult; + // #endregion } export interface ChatSessionItem { - /** - * Unique identifier for the chat session. - * - * @deprecated Will be replaced by `resource` - */ - id: string; - /** * The resource associated with the chat session. * * This is uniquely identifies the chat session and is used to open the chat session. */ - resource: Uri | undefined; + resource: Uri; /** * Human readable name of the session shown in the UI @@ -175,22 +172,28 @@ declare module 'vscode' { readonly requestHandler: ChatRequestHandler | undefined; } + /** + * Provides the content for a chat session rendered using the native chat UI. + */ export interface ChatSessionContentProvider { /** - * Resolves a chat session into a full `ChatSession` object. + * Provides the chat session content for a given uri. * - * @param sessionId The id of the chat session to open. + * The returned {@linkcode ChatSession} is used to populate the history of the chat UI. + * + * @param resource The URI of the chat session to resolve. * @param token A cancellation token that can be used to cancel the operation. + * + * @return The {@link ChatSession chat session} associated with the given URI. */ - provideChatSessionContent(sessionId: string, token: CancellationToken): Thenable | ChatSession; + provideChatSessionContent(resource: Uri, token: CancellationToken): Thenable | ChatSession; /** - * - * @param sessionId Identifier of the chat session being updated. + * @param resource Identifier of the chat session being updated. * @param updates Collection of option identifiers and their new values. Only the options that changed are included. * @param token A cancellation token that can be used to cancel the notification if the session is disposed. */ - provideHandleOptionsChange?(sessionId: string, updates: ReadonlyArray, token: CancellationToken): void; + provideHandleOptionsChange?(resource: Uri, updates: ReadonlyArray, token: CancellationToken): void; /** * Called as soon as you register (call me once) @@ -227,12 +230,12 @@ declare module 'vscode' { /** * Registers a new {@link ChatSessionContentProvider chat session content provider}. * - * @param chatSessionType A unique identifier for the chat session type. This is used to differentiate between different chat session providers. + * @param scheme The uri-scheme to register for. This must be unique. * @param provider The provider to register. * * @returns A disposable that unregisters the provider when disposed. */ - export function registerChatSessionContentProvider(chatSessionType: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; + export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; } export interface ChatContext { @@ -302,25 +305,4 @@ declare module 'vscode' { */ optionGroups?: ChatSessionProviderOptionGroup[]; } - - /** - * @deprecated - */ - export interface ChatSessionShowOptions { - /** - * The editor view column to show the chat session in. - * - * If not provided, the chat session will be shown in the chat panel instead. - */ - readonly viewColumn?: ViewColumn; - } - - export namespace window { - /** - * Shows a chat session in the panel or editor. - * - * @deprecated - */ - export function showChatSession(chatSessionType: string, sessionId: string, options: ChatSessionShowOptions): Thenable; - } }