diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 74800fefdee..994c8c8010f 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -197,9 +197,22 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA const contributedSession = chatSession?.contributedChatSession; let chatSessionContext: IChatSessionContextDto | undefined; if (contributedSession) { + let chatSessionResource = contributedSession.chatSessionResource; + let isUntitled = contributedSession.isUntitled; + + // For new untitled sessions, invoke the controller's newChatSessionItemHandler + // to let the extension create a proper session item before the first request. + if (isUntitled) { + const newItem = await this._chatSessionService.createNewChatSessionItem(contributedSession.chatSessionType, request, token); + if (newItem) { + chatSessionResource = newItem.resource; + isUntitled = false; + } + } + chatSessionContext = { - chatSessionResource: contributedSession.chatSessionResource, - isUntitled: contributedSession.isUntitled, + chatSessionResource, + isUntitled, initialSessionOptions: contributedSession.initialSessionOptions?.map(o => ({ optionId: o.optionId, value: typeof o.value === 'string' ? o.value : o.value.id, diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 87399cad640..cd45ead9ff5 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -347,6 +347,21 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes return this._proxy.$refreshChatSessionItems(this._handle, token); } + async newChatSessionItem(request: IChatAgentRequest, token: CancellationToken): Promise { + const dto = await raceCancellationError(this._proxy.$newChatSessionItem(this._handle, request, token), token); + if (!dto) { + return undefined; + } + const item: IChatSessionItem = { + ...dto, + resource: URI.revive(dto.resource), + changes: revive(dto.changes), + }; + this._items.set(item.resource, item); + this._onDidChangeChatSessionItems.fire(); + return item; + } + acceptChange(change: { readonly addedOrUpdated: readonly IChatSessionItem[]; readonly removed: readonly URI[] }): void { for (const item of change.addedOrUpdated) { this._items.set(item.resource, item); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 2025e31c436..14685130cb2 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3442,6 +3442,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { export interface ExtHostChatSessionsShape { $refreshChatSessionItems(providerHandle: number, token: CancellationToken): Promise; $onDidChangeChatSessionItemState(providerHandle: number, sessionResource: UriComponents, archived: boolean): void; + $newChatSessionItem(controllerHandle: number, request: Dto, token: CancellationToken): Promise | undefined>; $provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, token: CancellationToken): Promise; $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise; diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index e62fe643924..da032619287 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -379,6 +379,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio throw new Error('Not implemented for providers'); }, onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event, + newChatSessionItemHandler: undefined, dispose: () => { disposables.dispose(); }, @@ -422,6 +423,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio const disposables = new DisposableStore(); let isDisposed = false; + let newChatSessionItemHandler: vscode.ChatSessionItemController['newChatSessionItemHandler']; const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter()); const collection = new ChatSessionItemCollectionImpl(controllerHandle, this._proxy); @@ -451,6 +453,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }); return item; }, + get newChatSessionItemHandler() { return newChatSessionItemHandler; }, + set newChatSessionItemHandler(handler: vscode.ChatSessionItemController['newChatSessionItemHandler']) { newChatSessionItemHandler = handler; }, dispose: () => { isDisposed = true; disposables.dispose(); @@ -768,6 +772,29 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio await controllerData.controller.refreshHandler(token); } + async $newChatSessionItem(handle: number, request: IChatAgentRequest, token: CancellationToken): Promise | undefined> { + const controllerData = this._chatSessionItemControllers.get(handle); + if (!controllerData) { + this._logService.warn(`No controller found for handle ${handle}`); + return undefined; + } + + const handler = controllerData.controller.newChatSessionItemHandler; + if (!handler) { + return undefined; + } + + const model = await this.getModelForRequest(request, controllerData.extension); + const chatRequest = typeConvert.ChatAgentRequest.to(request, undefined, model, [], new Map(), controllerData.extension, this._logService); + + const item = await handler({ request: chatRequest }, token); + if (!item) { + return undefined; + } + + return typeConvert.ChatSessionItem.from(item); + } + $onDidChangeChatSessionItemState(controllerHandle: number, sessionResourceComponents: UriComponents, archived: boolean): void { const controllerData = this._chatSessionItemControllers.get(controllerHandle); if (!controllerData) { diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 2f846d3c7fb..78018645d73 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -62,6 +62,7 @@ suite('ObservableChatSession', function () { $disposeChatSessionContent: sinon.stub(), $refreshChatSessionItems: sinon.stub(), $onDidChangeChatSessionItemState: sinon.stub(), + $newChatSessionItem: sinon.stub().resolves(undefined), }; }); @@ -359,6 +360,7 @@ suite('MainThreadChatSessions', function () { $disposeChatSessionContent: sinon.stub(), $refreshChatSessionItems: sinon.stub(), $onDidChangeChatSessionItemState: sinon.stub(), + $newChatSessionItem: sinon.stub().resolves(undefined), }; const extHostContext = new class implements IExtHostContext { diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index bb70941b5ac..aab01ee1d49 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -29,7 +29,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; -import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js'; +import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentRequest, IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; @@ -992,6 +992,16 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return description ? renderAsPlaintext(description, { useLinkFormatter: true }) : ''; } + async createNewChatSessionItem(chatSessionType: string, request: IChatAgentRequest, token: CancellationToken): Promise { + const controllerData = this._itemControllers.get(chatSessionType); + if (!controllerData) { + return undefined; + } + + await controllerData.initialRefresh; + return controllerData.controller.newChatSessionItem?.(request, token); + } + public async getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise { { const existingSessionData = this._sessions.get(sessionResource); diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 8ccd21ac647..4c8f12a339f 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -198,6 +198,8 @@ export interface IChatSessionItemController { get items(): readonly IChatSessionItem[]; refresh(token: CancellationToken): Promise; + + newChatSessionItem?(request: IChatAgentRequest, token: CancellationToken): Promise; } /** @@ -293,6 +295,12 @@ export interface IChatSessionsService { registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable; getInProgressSessionDescription(chatModel: IChatModel): string | undefined; + + /** + * Creates a new chat session item using the controller's newChatSessionItemHandler. + * Returns undefined if the controller doesn't have a handler or if no controller is registered. + */ + createNewChatSessionItem(chatSessionType: string, request: IChatAgentRequest, token: CancellationToken): Promise; } export function isSessionInProgressStatus(state: ChatSessionStatus): boolean { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 932cbb036b0..bbeaa0b4c05 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -9,7 +9,7 @@ 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 { IChatAgentAttachmentCapabilities } from '../../common/participants/chatAgents.js'; +import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/participants/chatAgents.js'; import { IChatModel } from '../../common/model/chatModel.js'; import { IChatService } from '../../common/chatService/chatService.js'; import { IChatSession, IChatSessionContentProvider, IChatSessionItemController, IChatSessionItem, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; @@ -217,6 +217,10 @@ export class MockChatSessionsService implements IChatSessionsService { return undefined; } + async createNewChatSessionItem(_chatSessionType: string, _request: IChatAgentRequest, _token: CancellationToken): Promise { + return undefined; + } + registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable { // Store the emitter so tests can trigger it this.onChange = onChange; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 04df7ca03d7..5f93a7fe909 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -92,6 +92,15 @@ declare module 'vscode' { */ export type ChatSessionItemControllerRefreshHandler = (token: CancellationToken) => Thenable; + export interface ChatSessionItemControllerNewItemHandlerContext { + readonly request: ChatRequest; + } + + /** + * Extension callback invoked when a new chat session is started. + */ + export type ChatSessionItemControllerNewItemHandler = (context: ChatSessionItemControllerNewItemHandlerContext, token: CancellationToken) => Thenable; + /** * Manages chat sessions for a specific chat session type */ @@ -120,6 +129,15 @@ declare module 'vscode' { */ readonly refreshHandler: ChatSessionItemControllerRefreshHandler; + /** + * Invoked when a new chat session is started. + * + * This allows the controller to initialize the chat session item with information from the initial request. + * + * The returned chat session is added to the collection and shown in the UI. + */ + newChatSessionItemHandler?: ChatSessionItemControllerNewItemHandler; + /** * Fired when an item's archived state changes. */ @@ -363,6 +381,7 @@ declare module 'vscode' { */ // TODO: Should we introduce our own type for `ChatRequestHandler` since not all field apply to chat sessions? // TODO: Revisit this to align with code. + // TODO: pass in options? readonly requestHandler: ChatRequestHandler | undefined; }