Merge pull request #296121 from mjbvz/dev/mjbvz/mixed-mole

Add `newChatSessionItemHandler`  to let chat sessions create new session uris
This commit is contained in:
Matt Bierner
2026-02-18 14:06:08 -08:00
committed by GitHub
9 changed files with 103 additions and 4 deletions

View File

@@ -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,

View File

@@ -347,6 +347,21 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes
return this._proxy.$refreshChatSessionItems(this._handle, token);
}
async newChatSessionItem(request: IChatAgentRequest, token: CancellationToken): Promise<IChatSessionItem | undefined> {
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);

View File

@@ -3442,6 +3442,7 @@ export interface MainThreadChatSessionsShape extends IDisposable {
export interface ExtHostChatSessionsShape {
$refreshChatSessionItems(providerHandle: number, token: CancellationToken): Promise<void>;
$onDidChangeChatSessionItemState(providerHandle: number, sessionResource: UriComponents, archived: boolean): void;
$newChatSessionItem(controllerHandle: number, request: Dto<IChatAgentRequest>, token: CancellationToken): Promise<Dto<IChatSessionItem> | undefined>;
$provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, token: CancellationToken): Promise<ChatSessionDto>;
$interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise<void>;

View File

@@ -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<vscode.ChatSessionItem>());
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<ReturnType<typeof typeConvert.ChatSessionItem.from> | 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) {

View File

@@ -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 {

View File

@@ -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<IChatSessionItem | undefined> {
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<IChatSession> {
{
const existingSessionData = this._sessions.get(sessionResource);

View File

@@ -198,6 +198,8 @@ export interface IChatSessionItemController {
get items(): readonly IChatSessionItem[];
refresh(token: CancellationToken): Promise<void>;
newChatSessionItem?(request: IChatAgentRequest, token: CancellationToken): Promise<IChatSessionItem | undefined>;
}
/**
@@ -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<IChatSessionItem | undefined>;
}
export function isSessionInProgressStatus(state: ChatSessionStatus): boolean {

View File

@@ -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<IChatSessionItem | undefined> {
return undefined;
}
registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable {
// Store the emitter so tests can trigger it
this.onChange = onChange;

View File

@@ -92,6 +92,15 @@ declare module 'vscode' {
*/
export type ChatSessionItemControllerRefreshHandler = (token: CancellationToken) => Thenable<void>;
export interface ChatSessionItemControllerNewItemHandlerContext {
readonly request: ChatRequest;
}
/**
* Extension callback invoked when a new chat session is started.
*/
export type ChatSessionItemControllerNewItemHandler = (context: ChatSessionItemControllerNewItemHandlerContext, token: CancellationToken) => Thenable<ChatSessionItem>;
/**
* 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;
}