Path: blob/main/src/vs/workbench/api/browser/mainThreadChatSessions.ts
5228 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { raceCancellationError } from '../../../base/common/async.js';6import { CancellationToken } from '../../../base/common/cancellation.js';7import { Emitter, Event } from '../../../base/common/event.js';8import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js';9import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';10import { ResourceMap } from '../../../base/common/map.js';11import { revive } from '../../../base/common/marshalling.js';12import { autorun, IObservable, observableValue } from '../../../base/common/observable.js';13import { isEqual } from '../../../base/common/resources.js';14import { URI, UriComponents } from '../../../base/common/uri.js';15import { localize } from '../../../nls.js';16import { IDialogService } from '../../../platform/dialogs/common/dialogs.js';17import { ILogService } from '../../../platform/log/common/log.js';18import { hasValidDiff, IAgentSession } from '../../contrib/chat/browser/agentSessions/agentSessionsModel.js';19import { IAgentSessionsService } from '../../contrib/chat/browser/agentSessions/agentSessionsService.js';20import { IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js';21import { IChatEditorOptions } from '../../contrib/chat/browser/widgetHosts/editor/chatEditor.js';22import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/chatEditorInput.js';23import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js';24import { awaitStatsForSession } from '../../contrib/chat/common/chat.js';25import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js';26import { ChatSessionStatus, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js';27import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';28import { IChatModel } from '../../contrib/chat/common/model/chatModel.js';29import { IChatAgentRequest } from '../../contrib/chat/common/participants/chatAgents.js';30import { IChatTodoListService } from '../../contrib/chat/common/tools/chatTodoListService.js';31import { IEditorGroupsService } from '../../services/editor/common/editorGroupsService.js';32import { IEditorService } from '../../services/editor/common/editorService.js';33import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';34import { Dto } from '../../services/extensions/common/proxyIdentifier.js';35import { ExtHostChatSessionsShape, ExtHostContext, IChatProgressDto, IChatSessionHistoryItemDto, MainContext, MainThreadChatSessionsShape } from '../common/extHost.protocol.js';3637export class ObservableChatSession extends Disposable implements IChatSession {3839readonly sessionResource: URI;40readonly providerHandle: number;41readonly history: Array<IChatSessionHistoryItem>;42private _options?: Record<string, string | IChatSessionProviderOptionItem>;43public get options(): Record<string, string | IChatSessionProviderOptionItem> | undefined {44return this._options;45}46private readonly _progressObservable = observableValue<IChatProgress[]>(this, []);47private readonly _isCompleteObservable = observableValue<boolean>(this, false);4849private readonly _onWillDispose = new Emitter<void>();50readonly onWillDispose = this._onWillDispose.event;5152private readonly _pendingProgressChunks = new Map<string, IChatProgress[]>();53private _isInitialized = false;54private _interruptionWasCanceled = false;55private _disposalPending = false;5657private _initializationPromise?: Promise<void>;5859interruptActiveResponseCallback?: () => Promise<boolean>;60requestHandler?: (61request: IChatAgentRequest,62progress: (progress: IChatProgress[]) => void,63history: any[],64token: CancellationToken65) => Promise<void>;6667private readonly _proxy: ExtHostChatSessionsShape;68private readonly _providerHandle: number;69private readonly _logService: ILogService;70private readonly _dialogService: IDialogService;7172get progressObs(): IObservable<IChatProgress[]> {73return this._progressObservable;74}7576get isCompleteObs(): IObservable<boolean> {77return this._isCompleteObservable;78}7980constructor(81resource: URI,82providerHandle: number,83proxy: ExtHostChatSessionsShape,84logService: ILogService,85dialogService: IDialogService86) {87super();8889this.sessionResource = resource;90this.providerHandle = providerHandle;91this.history = [];92this._proxy = proxy;93this._providerHandle = providerHandle;94this._logService = logService;95this._dialogService = dialogService;96}9798initialize(token: CancellationToken): Promise<void> {99if (!this._initializationPromise) {100this._initializationPromise = this._doInitializeContent(token);101}102return this._initializationPromise;103}104105private async _doInitializeContent(token: CancellationToken): Promise<void> {106try {107const sessionContent = await raceCancellationError(108this._proxy.$provideChatSessionContent(this._providerHandle, this.sessionResource, token),109token110);111112this._options = sessionContent.options;113this.history.length = 0;114this.history.push(...sessionContent.history.map((turn: IChatSessionHistoryItemDto) => {115if (turn.type === 'request') {116const variables = turn.variableData?.variables.map(v => {117const entry = {118...v,119value: revive(v.value)120};121return entry as IChatRequestVariableEntry;122});123124return {125type: 'request' as const,126prompt: turn.prompt,127participant: turn.participant,128command: turn.command,129variableData: variables ? { variables } : undefined,130id: turn.id131};132}133134return {135type: 'response' as const,136parts: turn.parts.map((part: IChatProgressDto) => revive(part) as IChatProgress),137participant: turn.participant138};139}));140141if (sessionContent.hasActiveResponseCallback && !this.interruptActiveResponseCallback) {142this.interruptActiveResponseCallback = async () => {143const confirmInterrupt = () => {144if (this._disposalPending) {145this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionResource);146this._disposalPending = false;147}148this._proxy.$interruptChatSessionActiveResponse(this._providerHandle, this.sessionResource, 'ongoing');149return true;150};151152if (sessionContent.supportsInterruption) {153// If the session supports hot reload, interrupt without confirmation154return confirmInterrupt();155}156157// Prompt the user to confirm interruption158return this._dialogService.confirm({159message: localize('interruptActiveResponse', 'Are you sure you want to interrupt the active session?')160}).then(confirmed => {161if (confirmed.confirmed) {162// User confirmed interruption - dispose the session content on extension host163return confirmInterrupt();164} else {165// When user cancels the interruption, fire an empty progress message to keep the session alive166// This matches the behavior of the old implementation167this._addProgress([{168kind: 'progressMessage',169content: { value: '', isTrusted: false }170}]);171// Set flag to prevent completion when extension host calls handleProgressComplete172this._interruptionWasCanceled = true;173// User canceled interruption - cancel the deferred disposal174if (this._disposalPending) {175this._logService.info(`Canceling deferred disposal for session ${this.sessionResource} (user canceled interruption)`);176this._disposalPending = false;177}178return false;179}180});181};182}183184if (sessionContent.hasRequestHandler && !this.requestHandler) {185this.requestHandler = async (186request: IChatAgentRequest,187progress: (progress: IChatProgress[]) => void,188history: any[],189token: CancellationToken190) => {191// Clear previous progress and mark as active192this._progressObservable.set([], undefined);193this._isCompleteObservable.set(false, undefined);194195// Set up reactive progress observation before starting the request196let lastProgressLength = 0;197const progressDisposable = autorun(reader => {198const progressArray = this._progressObservable.read(reader);199const isComplete = this._isCompleteObservable.read(reader);200201if (progressArray.length > lastProgressLength) {202const newProgress = progressArray.slice(lastProgressLength);203progress(newProgress);204lastProgressLength = progressArray.length;205}206207if (isComplete) {208progressDisposable.dispose();209}210});211212try {213await this._proxy.$invokeChatSessionRequestHandler(this._providerHandle, this.sessionResource, request, history, token);214215// Only mark as complete if there's no active response callback216// Sessions with active response callbacks should only complete when explicitly told to via handleProgressComplete217if (!this._isCompleteObservable.get() && !this.interruptActiveResponseCallback) {218this._markComplete();219}220} catch (error) {221const errorProgress: IChatProgress = {222kind: 'progressMessage',223content: { value: `Error: ${error instanceof Error ? error.message : String(error)}`, isTrusted: false }224};225226this._addProgress([errorProgress]);227this._markComplete();228throw error;229} finally {230// Ensure progress observation is cleaned up231progressDisposable.dispose();232}233};234}235236this._isInitialized = true;237238// Process any pending progress chunks239const hasActiveResponse = sessionContent.hasActiveResponseCallback;240const hasRequestHandler = sessionContent.hasRequestHandler;241const hasAnyCapability = hasActiveResponse || hasRequestHandler;242243for (const [requestId, chunks] of this._pendingProgressChunks) {244this._logService.debug(`Processing ${chunks.length} pending progress chunks for session ${this.sessionResource}, requestId ${requestId}`);245this._addProgress(chunks);246}247this._pendingProgressChunks.clear();248249// If session has no active response callback and no request handler, mark it as complete250if (!hasAnyCapability) {251this._isCompleteObservable.set(true, undefined);252}253254} catch (error) {255this._logService.error(`Failed to initialize chat session ${this.sessionResource}:`, error);256throw error;257}258}259260/**261* Handle progress chunks coming from the extension host.262* If the session is not initialized yet, the chunks will be queued.263*/264handleProgressChunk(requestId: string, progress: IChatProgress[]): void {265if (!this._isInitialized) {266const existing = this._pendingProgressChunks.get(requestId) || [];267this._pendingProgressChunks.set(requestId, [...existing, ...progress]);268this._logService.debug(`Queuing ${progress.length} progress chunks for session ${this.sessionResource}, requestId ${requestId} (session not initialized)`);269return;270}271272this._addProgress(progress);273}274275/**276* Handle progress completion from the extension host.277*/278handleProgressComplete(requestId: string): void {279// Clean up any pending chunks for this request280this._pendingProgressChunks.delete(requestId);281282if (this._isInitialized) {283// Don't mark as complete if user canceled the interruption284if (!this._interruptionWasCanceled) {285this._markComplete();286} else {287// Reset the flag and don't mark as complete288this._interruptionWasCanceled = false;289}290}291}292293private _addProgress(progress: IChatProgress[]): void {294const currentProgress = this._progressObservable.get();295this._progressObservable.set([...currentProgress, ...progress], undefined);296}297298private _markComplete(): void {299if (!this._isCompleteObservable.get()) {300this._isCompleteObservable.set(true, undefined);301}302}303304override dispose(): void {305this._onWillDispose.fire();306this._onWillDispose.dispose();307this._pendingProgressChunks.clear();308309// If this session has an active response callback and disposal is happening,310// defer the actual session content disposal until we know the user's choice311if (this.interruptActiveResponseCallback && !this._interruptionWasCanceled) {312this._disposalPending = true;313// The actual disposal will happen in the interruption callback based on user's choice314} else {315// No active response callback or user already canceled interruption - dispose immediately316this._proxy.$disposeChatSessionContent(this._providerHandle, this.sessionResource);317}318super.dispose();319}320}321322@extHostNamedCustomer(MainContext.MainThreadChatSessions)323export class MainThreadChatSessions extends Disposable implements MainThreadChatSessionsShape {324private readonly _itemProvidersRegistrations = this._register(new DisposableMap<number, IDisposable & {325readonly provider: IChatSessionItemProvider;326readonly onDidChangeItems: Emitter<void>;327}>());328private readonly _contentProvidersRegistrations = this._register(new DisposableMap<number>());329private readonly _sessionTypeToHandle = new Map<string, number>();330331private readonly _activeSessions = new ResourceMap<ObservableChatSession>();332private readonly _sessionDisposables = new ResourceMap<IDisposable>();333334private readonly _proxy: ExtHostChatSessionsShape;335336constructor(337private readonly _extHostContext: IExtHostContext,338@IAgentSessionsService private readonly _agentSessionsService: IAgentSessionsService,339@IChatSessionsService private readonly _chatSessionsService: IChatSessionsService,340@IChatService private readonly _chatService: IChatService,341@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,342@IChatTodoListService private readonly _chatTodoListService: IChatTodoListService,343@IDialogService private readonly _dialogService: IDialogService,344@IEditorService private readonly _editorService: IEditorService,345@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,346@ILogService private readonly _logService: ILogService,347) {348super();349350this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions);351352this._register(this._chatSessionsService.onRequestNotifyExtension(({ sessionResource, updates, waitUntil }) => {353const handle = this._getHandleForSessionType(sessionResource.scheme);354this._logService.trace(`[MainThreadChatSessions] onRequestNotifyExtension received: scheme '${sessionResource.scheme}', handle ${handle}, ${updates.length} update(s)`);355if (handle !== undefined) {356waitUntil(this.notifyOptionsChange(handle, sessionResource, updates));357} else {358this._logService.warn(`[MainThreadChatSessions] Cannot notify option change for scheme '${sessionResource.scheme}': no provider registered. Registered schemes: [${Array.from(this._sessionTypeToHandle.keys()).join(', ')}]`);359}360}));361362this._register(this._agentSessionsService.model.onDidChangeSessionArchivedState(session => {363for (const [handle, { provider }] of this._itemProvidersRegistrations) {364if (provider.chatSessionType === session.providerType) {365this._proxy.$onDidChangeChatSessionItemState(handle, session.resource, session.isArchived());366}367}368}));369}370371private _getHandleForSessionType(chatSessionType: string): number | undefined {372return this._sessionTypeToHandle.get(chatSessionType);373}374375$registerChatSessionItemProvider(handle: number, chatSessionType: string): void {376// Register the provider handle - this tracks that a provider exists377const disposables = new DisposableStore();378const changeEmitter = disposables.add(new Emitter<void>());379const provider: IChatSessionItemProvider = {380chatSessionType,381onDidChangeChatSessionItems: Event.debounce(changeEmitter.event, (_, e) => e, 200),382provideChatSessionItems: (token) => this._provideChatSessionItems(handle, token),383};384disposables.add(this._chatSessionsService.registerChatSessionItemProvider(provider));385386this._itemProvidersRegistrations.set(handle, {387dispose: () => disposables.dispose(),388provider,389onDidChangeItems: changeEmitter,390});391392disposables.add(this._chatSessionsService.registerChatModelChangeListeners(393this._chatService,394chatSessionType,395() => changeEmitter.fire()396));397}398399$onDidChangeChatSessionItems(handle: number): void {400this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire();401}402403$onDidChangeChatSessionOptions(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string }>): void {404const sessionResource = URI.revive(sessionResourceComponents);405406this._chatSessionsService.notifySessionOptionsChange(sessionResource, updates);407}408409async $onDidCommitChatSessionItem(handle: number, originalComponents: UriComponents, modifiedCompoennts: UriComponents): Promise<void> {410const originalResource = URI.revive(originalComponents);411const modifiedResource = URI.revive(modifiedCompoennts);412413this._logService.trace(`$onDidCommitChatSessionItem: handle(${handle}), original(${originalResource}), modified(${modifiedResource})`);414const chatSessionType = this._itemProvidersRegistrations.get(handle)?.provider.chatSessionType;415if (!chatSessionType) {416this._logService.error(`No chat session type found for provider handle ${handle}`);417return;418}419420const originalEditor = this._editorService.editors.find(editor => editor.resource?.toString() === originalResource.toString());421const originalModel = this._chatService.getActiveSessionReference(originalResource);422const contribution = this._chatSessionsService.getAllChatSessionContributions().find(c => c.type === chatSessionType);423424try {425426// Migrate todos from old session to new session427this._chatTodoListService.migrateTodos(originalResource, modifiedResource);428429// Find the group containing the original editor430const originalGroup =431this.editorGroupService.groups.find(group => group.editors.some(editor => isEqual(editor.resource, originalResource)))432?? this.editorGroupService.activeGroup;433434const options: IChatEditorOptions = {435title: {436preferred: originalEditor?.getName() || undefined,437fallback: localize('chatEditorContributionName', "{0}", contribution?.displayName),438}439};440441// Prefetch the chat session content to make the subsequent editor swap quick442const newSession = await this._chatSessionsService.getOrCreateChatSession(443URI.revive(modifiedResource),444CancellationToken.None,445);446447if (originalEditor) {448newSession.transferredState = originalEditor instanceof ChatEditorInput449? { editingSession: originalEditor.transferOutEditingSession(), inputState: originalModel?.object?.inputModel.toJSON() }450: undefined;451452await this._editorService.replaceEditors([{453editor: originalEditor,454replacement: {455resource: modifiedResource,456options,457},458}], originalGroup);459return;460}461462// If chat editor is in the side panel, then those are not listed as editors.463// In that case we need to transfer editing session using the original model.464if (originalModel) {465newSession.transferredState = {466editingSession: originalModel.object.editingSession,467inputState: originalModel.object.inputModel.toJSON()468};469}470471const chatViewWidget = this._chatWidgetService.getWidgetBySessionResource(originalResource);472if (chatViewWidget && isIChatViewViewContext(chatViewWidget.viewContext)) {473await this._chatWidgetService.openSession(modifiedResource, undefined, { preserveFocus: true });474} else {475// Loading the session to ensure the session is created and editing session is transferred.476const ref = await this._chatService.loadSessionForResource(modifiedResource, ChatAgentLocation.Chat, CancellationToken.None);477ref?.dispose();478}479} finally {480originalModel?.dispose();481}482}483484private async _provideChatSessionItems(handle: number, token: CancellationToken): Promise<IChatSessionItem[]> {485try {486// Get all results as an array from the RPC call487const sessions = await this._proxy.$provideChatSessionItems(handle, token);488return Promise.all(sessions.map(async session => {489const uri = URI.revive(session.resource);490const model = this._chatService.getSession(uri);491if (model) {492session = await this.handleSessionModelOverrides(model, session);493}494495// We can still get stats if there is no model or if fetching from model failed496if (!session.changes || !model) {497const stats = (await this._chatService.getMetadataForSession(uri))?.stats;498// TODO: we shouldn't be converting this, the types should match499const diffs: IAgentSession['changes'] = {500files: stats?.fileCount || 0,501insertions: stats?.added || 0,502deletions: stats?.removed || 0503};504if (hasValidDiff(diffs)) {505session.changes = diffs;506}507}508509return {510...session,511changes: revive(session.changes),512resource: uri,513iconPath: session.iconPath,514tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined,515archived: session.archived,516} satisfies IChatSessionItem;517}));518} catch (error) {519this._logService.error('Error providing chat sessions:', error);520}521return [];522}523524private async handleSessionModelOverrides(model: IChatModel, session: Dto<IChatSessionItem>): Promise<Dto<IChatSessionItem>> {525// Override desciription if there's an in-progress count526const inProgress = model.getRequests().filter(r => r.response && !r.response.isComplete);527if (inProgress.length) {528session.description = this._chatSessionsService.getInProgressSessionDescription(model);529}530531// Override changes532// TODO: @osortega we don't really use statistics anymore, we need to clarify that in the API533if (!(session.changes instanceof Array)) {534const modelStats = await awaitStatsForSession(model);535if (modelStats) {536session.changes = {537files: modelStats.fileCount,538insertions: modelStats.added,539deletions: modelStats.removed540};541}542}543544// Override status if the models needs input545if (model.lastRequest?.response?.state === ResponseModelState.NeedsInput) {546session.status = ChatSessionStatus.NeedsInput;547}548549return session;550}551552private async _provideChatSessionContent(providerHandle: number, sessionResource: URI, token: CancellationToken): Promise<IChatSession> {553let session = this._activeSessions.get(sessionResource);554555if (!session) {556session = new ObservableChatSession(557sessionResource,558providerHandle,559this._proxy,560this._logService,561this._dialogService562);563this._activeSessions.set(sessionResource, session);564const disposable = session.onWillDispose(() => {565this._activeSessions.delete(sessionResource);566this._sessionDisposables.get(sessionResource)?.dispose();567this._sessionDisposables.delete(sessionResource);568});569this._sessionDisposables.set(sessionResource, disposable);570}571572try {573await session.initialize(token);574if (session.options) {575for (const [_, handle] of this._sessionTypeToHandle) {576if (handle === providerHandle) {577for (const [optionId, value] of Object.entries(session.options)) {578this._chatSessionsService.setSessionOption(sessionResource, optionId, value);579}580break;581}582}583}584return session;585} catch (error) {586session.dispose();587this._logService.error(`Error providing chat session content for handle ${providerHandle} and resource ${sessionResource.toString()}:`, error);588throw error;589}590}591592$unregisterChatSessionItemProvider(handle: number): void {593this._itemProvidersRegistrations.deleteAndDispose(handle);594}595596$registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void {597const provider: IChatSessionContentProvider = {598provideChatSessionContent: (resource, token) => this._provideChatSessionContent(handle, resource, token)599};600601this._sessionTypeToHandle.set(chatSessionScheme, handle);602this._contentProvidersRegistrations.set(handle, this._chatSessionsService.registerChatSessionContentProvider(chatSessionScheme, provider));603this._refreshProviderOptions(handle, chatSessionScheme);604}605606$unregisterChatSessionContentProvider(handle: number): void {607this._contentProvidersRegistrations.deleteAndDispose(handle);608for (const [sessionType, h] of this._sessionTypeToHandle) {609if (h === handle) {610this._sessionTypeToHandle.delete(sessionType);611break;612}613}614615// dispose all sessions from this provider and clean up its disposables616for (const [key, session] of this._activeSessions) {617if (session.providerHandle === handle) {618session.dispose();619this._activeSessions.delete(key);620}621}622}623624async $handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise<void> {625const resource = URI.revive(sessionResource);626const observableSession = this._activeSessions.get(resource);627if (!observableSession) {628this._logService.warn(`No session found for progress chunks: handle ${handle}, sessionResource ${resource}, requestId ${requestId}`);629return;630}631632const chatProgressParts: IChatProgress[] = chunks.map(chunk => {633const [progress] = Array.isArray(chunk) ? chunk : [chunk];634return revive(progress) as IChatProgress;635});636637observableSession.handleProgressChunk(requestId, chatProgressParts);638}639640$handleProgressComplete(handle: number, sessionResource: UriComponents, requestId: string) {641const resource = URI.revive(sessionResource);642const observableSession = this._activeSessions.get(resource);643if (!observableSession) {644this._logService.warn(`No session found for progress completion: handle ${handle}, sessionResource ${resource}, requestId ${requestId}`);645return;646}647648observableSession.handleProgressComplete(requestId);649}650651$handleAnchorResolve(handle: number, sesssionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto<IChatContentInlineReference>): void {652// throw new Error('Method not implemented.');653}654655$onDidChangeChatSessionProviderOptions(handle: number): void {656let sessionType: string | undefined;657for (const [type, h] of this._sessionTypeToHandle) {658if (h === handle) {659sessionType = type;660break;661}662}663664if (!sessionType) {665this._logService.warn(`No session type found for chat session content provider handle ${handle} when refreshing provider options`);666return;667}668669this._refreshProviderOptions(handle, sessionType);670}671672private _refreshProviderOptions(handle: number, chatSessionScheme: string): void {673this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => {674if (options?.optionGroups && options.optionGroups.length) {675const groupsWithCallbacks = options.optionGroups.map(group => ({676...group,677onSearch: group.searchable ? async (query: string, token: CancellationToken) => {678return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token);679} : undefined,680}));681this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, groupsWithCallbacks);682}683}).catch(err => this._logService.error('Error fetching chat session options', err));684}685686override dispose(): void {687for (const session of this._activeSessions.values()) {688session.dispose();689}690this._activeSessions.clear();691692for (const disposable of this._sessionDisposables.values()) {693disposable.dispose();694}695this._sessionDisposables.clear();696697super.dispose();698}699700private _reviveTooltip(tooltip: string | IMarkdownString | undefined): string | MarkdownString | undefined {701if (!tooltip) {702return undefined;703}704705// If it's already a string, return as-is706if (typeof tooltip === 'string') {707return tooltip;708}709710// If it's a serialized IMarkdownString, revive it to MarkdownString711if (typeof tooltip === 'object' && 'value' in tooltip) {712return MarkdownString.lift(tooltip);713}714715return undefined;716}717718/**719* Notify the extension about option changes for a session720*/721async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>): Promise<void> {722this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: starting proxy call for handle ${handle}, sessionResource ${sessionResource}`);723try {724await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None);725this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: proxy call completed for handle ${handle}, sessionResource ${sessionResource}`);726} catch (error) {727this._logService.error(`[MainThreadChatSessions] notifyOptionsChange: error for handle ${handle}, sessionResource ${sessionResource}:`, error);728}729}730}731732733