Path: blob/main/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts
3296 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 { DeferredPromise } from '../../../../base/common/async.js';6import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';7import { memoize } from '../../../../base/common/decorators.js';8import { toErrorMessage } from '../../../../base/common/errorMessage.js';9import { ErrorNoTelemetry } from '../../../../base/common/errors.js';10import { Emitter, Event } from '../../../../base/common/event.js';11import { MarkdownString } from '../../../../base/common/htmlContent.js';12import { Iterable } from '../../../../base/common/iterator.js';13import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';14import { revive } from '../../../../base/common/marshalling.js';15import { autorun, derived, IObservable, ObservableMap } from '../../../../base/common/observable.js';16import { StopWatch } from '../../../../base/common/stopwatch.js';17import { URI } from '../../../../base/common/uri.js';18import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';19import { localize } from '../../../../nls.js';20import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { ILogService } from '../../../../platform/log/common/log.js';23import { Progress } from '../../../../platform/progress/common/progress.js';24import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';25import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';26import { IExtensionService } from '../../../services/extensions/common/extensions.js';27import { IMcpService } from '../../mcp/common/mcpTypes.js';28import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js';29import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js';30import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js';31import { ChatRequestParser } from './chatRequestParser.js';32import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js';33import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js';34import { IChatSessionsService } from './chatSessionsService.js';35import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js';36import { IChatSlashCommandService } from './chatSlashCommands.js';37import { IChatTransferService } from './chatTransferService.js';38import { ChatSessionUri } from './chatUri.js';39import { IChatRequestVariableEntry } from './chatVariableEntries.js';40import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js';41import { ChatMessageRole, IChatMessage } from './languageModels.js';42import { ILanguageModelToolsService } from './languageModelToolsService.js';4344const serializedChatKey = 'interactive.sessions';4546const globalChatKey = 'chat.workspaceTransfer';4748const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60;4950const maxPersistedSessions = 25;5152class CancellableRequest implements IDisposable {53constructor(54public readonly cancellationTokenSource: CancellationTokenSource,55public requestId: string | undefined,56@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService57) { }5859dispose() {60this.cancellationTokenSource.dispose();61}6263cancel() {64if (this.requestId) {65this.toolsService.cancelToolCallsForRequest(this.requestId);66}6768this.cancellationTokenSource.cancel();69}70}7172export class ChatService extends Disposable implements IChatService {73declare _serviceBrand: undefined;7475private readonly _sessionModels = new ObservableMap<string, ChatModel>();76private readonly _contentProviderSessionModels = new Map<string, Map<string, { readonly model: IChatModel; readonly disposables: DisposableStore }>>();77private readonly _pendingRequests = this._register(new DisposableMap<string, CancellableRequest>());78private _persistedSessions: ISerializableChatsData;7980/** Just for empty windows, need to enforce that a chat was deleted, even though other windows still have it */81private _deletedChatIds = new Set<string>();8283private _transferredSessionData: IChatTransferredSessionData | undefined;84public get transferredSessionData(): IChatTransferredSessionData | undefined {85return this._transferredSessionData;86}8788private readonly _onDidSubmitRequest = this._register(new Emitter<{ chatSessionId: string }>());89public readonly onDidSubmitRequest: Event<{ chatSessionId: string }> = this._onDidSubmitRequest.event;9091private readonly _onDidPerformUserAction = this._register(new Emitter<IChatUserActionEvent>());92public readonly onDidPerformUserAction: Event<IChatUserActionEvent> = this._onDidPerformUserAction.event;9394private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; reason: 'cleared' }>());95public readonly onDidDisposeSession = this._onDidDisposeSession.event;9697private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap<string, CancellationTokenSource>());98private readonly _chatServiceTelemetry: ChatServiceTelemetry;99private readonly _chatSessionStore: ChatSessionStore;100101readonly requestInProgressObs: IObservable<boolean>;102103@memoize104private get useFileStorage(): boolean {105return this.configurationService.getValue(ChatConfiguration.UseFileStorage);106}107108public get edits2Enabled(): boolean {109return this.configurationService.getValue(ChatConfiguration.Edits2Enabled);110}111112private get isEmptyWindow(): boolean {113const workspace = this.workspaceContextService.getWorkspace();114return !workspace.configuration && workspace.folders.length === 0;115}116117constructor(118@IStorageService private readonly storageService: IStorageService,119@ILogService private readonly logService: ILogService,120@IExtensionService private readonly extensionService: IExtensionService,121@IInstantiationService private readonly instantiationService: IInstantiationService,122@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,123@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,124@IChatAgentService private readonly chatAgentService: IChatAgentService,125@IConfigurationService private readonly configurationService: IConfigurationService,126@IChatTransferService private readonly chatTransferService: IChatTransferService,127@IChatSessionsService private readonly chatSessionService: IChatSessionsService,128@IMcpService private readonly mcpService: IMcpService,129) {130super();131132this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry);133134const sessionData = storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, '');135if (sessionData) {136this._persistedSessions = this.deserializeChats(sessionData);137const countsForLog = Object.keys(this._persistedSessions).length;138if (countsForLog > 0) {139this.trace('constructor', `Restored ${countsForLog} persisted sessions`);140}141} else {142this._persistedSessions = {};143}144145const transferredData = this.getTransferredSessionData();146const transferredChat = transferredData?.chat;147if (transferredChat) {148this.trace('constructor', `Transferred session ${transferredChat.sessionId}`);149this._persistedSessions[transferredChat.sessionId] = transferredChat;150this._transferredSessionData = {151sessionId: transferredChat.sessionId,152inputValue: transferredData.inputValue,153location: transferredData.location,154mode: transferredData.mode,155};156}157158this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore));159if (this.useFileStorage) {160this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions);161162// When using file storage, populate _persistedSessions with session metadata from the index163// This ensures that getPersistedSessionTitle() can find titles for inactive sessions164this.initializePersistedSessionsFromFileStorage();165}166167this._register(storageService.onWillSaveState(() => this.saveState()));168169this.requestInProgressObs = derived(reader => {170const models = this._sessionModels.observable.read(reader).values();171return Array.from(models).some(model => model.requestInProgressObs.read(reader));172});173}174175isEnabled(location: ChatAgentLocation): boolean {176return this.chatAgentService.getContributedDefaultAgent(location) !== undefined;177}178179private saveState(): void {180const liveChats = Array.from(this._sessionModels.values())181.filter(session =>182this.shouldSaveToHistory(session.sessionId) && (session.initialLocation === ChatAgentLocation.Panel || session.initialLocation === ChatAgentLocation.Editor));183184if (this.useFileStorage) {185this._chatSessionStore.storeSessions(liveChats);186} else {187if (this.isEmptyWindow) {188this.syncEmptyWindowChats(liveChats);189} else {190let allSessions: (ChatModel | ISerializableChatData)[] = liveChats;191allSessions = allSessions.concat(192Object.values(this._persistedSessions)193.filter(session => !this._sessionModels.has(session.sessionId))194.filter(session => session.requests.length));195allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0));196197allSessions = allSessions.slice(0, maxPersistedSessions);198199if (allSessions.length) {200this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`);201}202203const serialized = JSON.stringify(allSessions);204205if (allSessions.length) {206this.trace('onWillSaveState', `Persisting ${serialized.length} chars`);207}208209this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE);210}211212}213214this._deletedChatIds.clear();215}216217private syncEmptyWindowChats(thisWindowChats: ChatModel[]): void {218// Note- an unavoidable race condition exists here. If there are multiple empty windows open, and the user quits the application, then the focused219// window may lose active chats, because all windows are reading and writing to storageService at the same time. This can't be fixed without some220// kind of locking, but in reality, the focused window will likely have run `saveState` at some point, like on a window focus change, and it will221// generally be fine.222const sessionData = this.storageService.get(serializedChatKey, StorageScope.APPLICATION, '');223224const originalPersistedSessions = this._persistedSessions;225let persistedSessions: ISerializableChatsData;226if (sessionData) {227persistedSessions = this.deserializeChats(sessionData);228const countsForLog = Object.keys(persistedSessions).length;229if (countsForLog > 0) {230this.trace('constructor', `Restored ${countsForLog} persisted sessions`);231}232} else {233persistedSessions = {};234}235236this._deletedChatIds.forEach(id => delete persistedSessions[id]);237238// Has the chat in this window been updated, and then closed? Overwrite the old persisted chats.239Object.values(originalPersistedSessions).forEach(session => {240const persistedSession = persistedSessions[session.sessionId];241if (persistedSession && session.requests.length > persistedSession.requests.length) {242// We will add a 'modified date' at some point, but comparing the number of requests is good enough243persistedSessions[session.sessionId] = session;244} else if (!persistedSession && session.isNew) {245// This session was created in this window, and hasn't been persisted yet246session.isNew = false;247persistedSessions[session.sessionId] = session;248}249});250251this._persistedSessions = persistedSessions;252253// Add this window's active chat models to the set to persist.254// Having the same session open in two empty windows at the same time can lead to data loss, this is acceptable255const allSessions: Record<string, ISerializableChatData | ChatModel> = { ...this._persistedSessions };256for (const chat of thisWindowChats) {257allSessions[chat.sessionId] = chat;258}259260let sessionsList = Object.values(allSessions);261sessionsList.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0));262sessionsList = sessionsList.slice(0, maxPersistedSessions);263const data = JSON.stringify(sessionsList);264this.storageService.store(serializedChatKey, data, StorageScope.APPLICATION, StorageTarget.MACHINE);265}266267notifyUserAction(action: IChatUserActionEvent): void {268this._chatServiceTelemetry.notifyUserAction(action);269this._onDidPerformUserAction.fire(action);270if (action.action.kind === 'chatEditingSessionAction') {271const model = this._sessionModels.get(action.sessionId);272if (model) {273model.notifyEditingAction(action.action);274}275}276}277278async setChatSessionTitle(sessionId: string, title: string): Promise<void> {279const model = this._sessionModels.get(sessionId);280if (model) {281model.setCustomTitle(title);282}283284// Also update the persisted session data285if (this.useFileStorage) {286// Update the title in the file storage287await this._chatSessionStore.setSessionTitle(sessionId, title);288// Trigger immediate save to ensure consistency289await this.saveState();290} else {291// Update the in-memory storage292if (this._persistedSessions[sessionId]) {293this._persistedSessions[sessionId].customTitle = title;294} else {295// Create a minimal placeholder entry with the title296// The full session data will be merged later when the session is activated or saved297this._persistedSessions[sessionId] = {298version: 3,299sessionId: sessionId,300customTitle: title,301creationDate: Date.now(),302lastMessageDate: Date.now(),303isImported: false,304initialLocation: undefined,305requests: [],306requesterUsername: '',307responderUsername: '',308requesterAvatarIconUri: undefined,309responderAvatarIconUri: undefined,310};311}312313// Trigger immediate save to ensure the title is persisted314await this.saveState();315}316}317318private trace(method: string, message?: string): void {319if (message) {320this.logService.trace(`ChatService#${method}: ${message}`);321} else {322this.logService.trace(`ChatService#${method}`);323}324}325326private error(method: string, message: string): void {327this.logService.error(`ChatService#${method} ${message}`);328}329330private deserializeChats(sessionData: string): ISerializableChatsData {331try {332const arrayOfSessions: ISerializableChatDataIn[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data333if (!Array.isArray(arrayOfSessions)) {334throw new Error('Expected array');335}336337const sessions = arrayOfSessions.reduce<ISerializableChatsData>((acc, session) => {338// Revive serialized markdown strings in response data339for (const request of session.requests) {340if (Array.isArray(request.response)) {341request.response = request.response.map((response) => {342if (typeof response === 'string') {343return new MarkdownString(response);344}345return response;346});347} else if (typeof request.response === 'string') {348request.response = [new MarkdownString(request.response)];349}350}351352acc[session.sessionId] = normalizeSerializableChatData(session);353return acc;354}, {});355return sessions;356} catch (err) {357this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`);358return {};359}360}361362private getTransferredSessionData(): IChatTransfer2 | undefined {363const data: IChatTransfer2[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []);364const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri;365if (!workspaceUri) {366return;367}368369const thisWorkspace = workspaceUri.toString();370const currentTime = Date.now();371// Only use transferred data if it was created recently372const transferred = data.find(item => URI.revive(item.toWorkspace).toString() === thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS));373// Keep data that isn't for the current workspace and that hasn't expired yet374const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS));375this.storageService.store(globalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE);376return transferred;377}378379private async initializePersistedSessionsFromFileStorage(): Promise<void> {380381const index = await this._chatSessionStore.getIndex();382const sessionIds = Object.keys(index);383384for (const sessionId of sessionIds) {385const metadata = index[sessionId];386if (metadata && !this._persistedSessions[sessionId]) {387// Create a minimal session entry with the title information388// This allows getPersistedSessionTitle() to find the title without loading the full session389const minimalSession: ISerializableChatData = {390version: 3,391sessionId: sessionId,392customTitle: metadata.title,393creationDate: Date.now(), // Use current time as fallback394lastMessageDate: metadata.lastMessageDate,395isImported: metadata.isImported || false,396initialLocation: metadata.initialLocation,397requests: [], // Empty requests array - this is just for title lookup398requesterUsername: '',399responderUsername: '',400requesterAvatarIconUri: undefined,401responderAvatarIconUri: undefined,402};403404this._persistedSessions[sessionId] = minimalSession;405}406}407}408409/**410* Returns an array of chat details for all persisted chat sessions that have at least one request.411* Chat sessions that have already been loaded into the chat view are excluded from the result.412* Imported chat sessions are also excluded from the result.413*/414async getHistory(): Promise<IChatDetail[]> {415if (this.useFileStorage) {416const liveSessionItems = Array.from(this._sessionModels.values())417.filter(session => !session.isImported)418.map(session => {419const title = session.title || localize('newChat', "New Chat");420return {421sessionId: session.sessionId,422title,423lastMessageDate: session.lastMessageDate,424isActive: true,425} satisfies IChatDetail;426});427428const index = await this._chatSessionStore.getIndex();429const entries = Object.values(index)430.filter(entry => !this._sessionModels.has(entry.sessionId) && !entry.isImported && !entry.isEmpty)431.map((entry): IChatDetail => ({432...entry,433isActive: this._sessionModels.has(entry.sessionId),434}));435return [...liveSessionItems, ...entries];436}437438const persistedSessions = Object.values(this._persistedSessions)439.filter(session => session.requests.length > 0)440.filter(session => !this._sessionModels.has(session.sessionId));441442const persistedSessionItems = persistedSessions443.filter(session => !session.isImported)444.map(session => {445const title = session.customTitle ?? ChatModel.getDefaultTitle(session.requests);446return {447sessionId: session.sessionId,448title,449lastMessageDate: session.lastMessageDate,450isActive: false,451} satisfies IChatDetail;452});453const liveSessionItems = Array.from(this._sessionModels.values())454.filter(session => !session.isImported)455.map(session => {456const title = session.title || localize('newChat', "New Chat");457return {458sessionId: session.sessionId,459title,460lastMessageDate: session.lastMessageDate,461isActive: true,462} satisfies IChatDetail;463});464return [...liveSessionItems, ...persistedSessionItems];465}466467async removeHistoryEntry(sessionId: string): Promise<void> {468if (this.useFileStorage) {469await this._chatSessionStore.deleteSession(sessionId);470return;471}472473if (this._persistedSessions[sessionId]) {474this._deletedChatIds.add(sessionId);475delete this._persistedSessions[sessionId];476this.saveState();477}478}479480async clearAllHistoryEntries(): Promise<void> {481if (this.useFileStorage) {482await this._chatSessionStore.clearAllSessions();483return;484}485486Object.values(this._persistedSessions).forEach(session => this._deletedChatIds.add(session.sessionId));487this._persistedSessions = {};488this.saveState();489}490491startSession(location: ChatAgentLocation, token: CancellationToken, isGlobalEditingSession: boolean = true): ChatModel {492this.trace('startSession');493return this._startSession(undefined, location, isGlobalEditingSession, token);494}495496private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: ChatAgentLocation, isGlobalEditingSession: boolean, token: CancellationToken): ChatModel {497const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, location);498if (location === ChatAgentLocation.Panel) {499model.startEditingSession(isGlobalEditingSession);500}501502this._sessionModels.set(model.sessionId, model);503this.initializeSession(model, token);504return model;505}506507private initializeSession(model: ChatModel, token: CancellationToken): void {508this.trace('initializeSession', `Initialize session ${model.sessionId}`);509510// Activate the default extension provided agent but do not wait511// for it to be ready so that the session can be used immediately512// without having to wait for the agent to be ready.513this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e));514}515516async activateDefaultAgent(location: ChatAgentLocation): Promise<void> {517await this.extensionService.whenInstalledExtensionsRegistered();518519const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Panel);520if (!defaultAgentData) {521throw new ErrorNoTelemetry('No default agent contributed');522}523524// Await activation of the extension provided agent525// Using `activateById` as workaround for the issue526// https://github.com/microsoft/vscode/issues/250590527if (!defaultAgentData.isCore) {528await this.extensionService.activateById(defaultAgentData.extensionId, {529activationEvent: `onChatParticipant:${defaultAgentData.id}`,530extensionId: defaultAgentData.extensionId,531startup: false532});533}534535const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id);536if (!defaultAgent) {537throw new ErrorNoTelemetry('No default agent registered');538}539}540541getSession(sessionId: string): IChatModel | undefined {542return this._sessionModels.get(sessionId);543}544545async getOrRestoreSession(sessionId: string): Promise<ChatModel | undefined> {546this.trace('getOrRestoreSession', `sessionId: ${sessionId}`);547const model = this._sessionModels.get(sessionId);548if (model) {549return model;550}551552let sessionData: ISerializableChatData | undefined;553if (!this.useFileStorage || this.transferredSessionData?.sessionId === sessionId) {554sessionData = revive(this._persistedSessions[sessionId]);555} else {556sessionData = revive(await this._chatSessionStore.readSession(sessionId));557}558559if (!sessionData) {560return undefined;561}562563const session = this._startSession(sessionData, sessionData.initialLocation ?? ChatAgentLocation.Panel, true, CancellationToken.None);564565const isTransferred = this.transferredSessionData?.sessionId === sessionId;566if (isTransferred) {567this._transferredSessionData = undefined;568}569570return session;571}572573/**574* This is really just for migrating data from the edit session location to the panel.575*/576isPersistedSessionEmpty(sessionId: string): boolean {577const session = this._persistedSessions[sessionId];578if (session) {579return session.requests.length === 0;580}581582return this._chatSessionStore.isSessionEmpty(sessionId);583}584585getPersistedSessionTitle(sessionId: string): string | undefined {586// First check the memory cache (_persistedSessions)587const session = this._persistedSessions[sessionId];588if (session) {589const title = session.customTitle || ChatModel.getDefaultTitle(session.requests);590return title;591}592593// If using file storage and not found in memory, try to read directly from file storage index594// This handles the case where getName() is called before initialization completes595if (this.useFileStorage) {596// Access the internal synchronous index method via reflection597// This is a workaround for the timing issue where initialization hasn't completed598const internalGetIndex = (this._chatSessionStore as any).internalGetIndex;599if (typeof internalGetIndex === 'function') {600const indexData = internalGetIndex.call(this._chatSessionStore);601const metadata = indexData.entries[sessionId];602if (metadata && metadata.title) {603return metadata.title;604}605}606}607608return undefined;609}610611loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined {612return this._startSession(data, data.initialLocation ?? ChatAgentLocation.Panel, true, CancellationToken.None);613}614615async loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise<IChatModel | undefined> {616// TODO: Move this into a new ChatModelService617const parsed = ChatSessionUri.parse(resource);618if (!parsed) {619throw new Error('Invalid chat session URI');620}621622const existing = this._contentProviderSessionModels.get(parsed.chatSessionType)?.get(parsed.sessionId);623if (existing) {624return existing.model;625}626627if (parsed.chatSessionType === 'local') {628return this.getOrRestoreSession(parsed.sessionId);629}630631const chatSessionType = parsed.chatSessionType;632const content = await this.chatSessionService.provideChatSessionContent(chatSessionType, parsed.sessionId, CancellationToken.None);633634const model = this._startSession(undefined, location, true, CancellationToken.None);635if (!this._contentProviderSessionModels.has(chatSessionType)) {636this._contentProviderSessionModels.set(chatSessionType, new Map());637}638const disposables = new DisposableStore();639this._contentProviderSessionModels.get(chatSessionType)!.set(parsed.sessionId, { model, disposables });640641disposables.add(model.onDidDispose(() => {642this._contentProviderSessionModels?.get(chatSessionType)?.delete(parsed.sessionId);643content.dispose();644}));645646let lastRequest: ChatRequestModel | undefined;647for (const message of content.history) {648if (message.type === 'request') {649if (lastRequest) {650model.completeResponse(lastRequest);651}652653const requestText = message.prompt;654655const parsedRequest: IParsedChatRequest = {656text: requestText,657parts: [new ChatRequestTextPart(658new OffsetRange(0, requestText.length),659{ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: requestText.length + 1 },660requestText661)]662};663const agent =664message.participant665? this.chatAgentService.getAgent(message.participant) // TODO(jospicer): Remove and always hardcode?666: this.chatAgentService.getAgent(chatSessionType);667lastRequest = model.addRequest(parsedRequest,668{ variables: [] }, // variableData6690, // attempt670undefined,671agent,672undefined, // slashCommand673undefined, // confirmation674undefined, // locationData675undefined, // attachments676true // isCompleteAddedRequest - this indicates it's a complete request, not user input677);678} else {679// response680if (lastRequest) {681for (const part of message.parts) {682model.acceptResponseProgress(lastRequest, part);683}684}685}686}687688if (content.progressObs && lastRequest && content.interruptActiveResponseCallback) {689const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);690this._pendingRequests.set(model.sessionId, initialCancellationRequest);691const cancellationListener = new MutableDisposable();692693const createCancellationListener = (token: CancellationToken) => {694return token.onCancellationRequested(() => {695content.interruptActiveResponseCallback?.().then(userConfirmedInterruption => {696if (!userConfirmedInterruption) {697// User cancelled the interruption698const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined);699this._pendingRequests.set(model.sessionId, newCancellationRequest);700cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token);701}702});703});704};705706cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token);707disposables.add(cancellationListener);708709let lastProgressLength = 0;710disposables.add(autorun(reader => {711const progressArray = content.progressObs?.read(reader) ?? [];712const isComplete = content.isCompleteObs?.read(reader) ?? false;713714// Process only new progress items715if (progressArray.length > lastProgressLength) {716const newProgress = progressArray.slice(lastProgressLength);717for (const progress of newProgress) {718model?.acceptResponseProgress(lastRequest, progress);719}720lastProgressLength = progressArray.length;721}722723// Handle completion724if (isComplete) {725model?.completeResponse(lastRequest);726cancellationListener.clear();727}728}));729} else {730if (lastRequest) {731model.completeResponse(lastRequest);732}733}734735return model;736}737738async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise<void> {739const model = this._sessionModels.get(request.session.sessionId);740if (!model && model !== request.session) {741throw new Error(`Unknown session: ${request.session.sessionId}`);742}743744const cts = this._pendingRequests.get(request.session.sessionId);745if (cts) {746this.trace('resendRequest', `Session ${request.session.sessionId} already has a pending request, cancelling...`);747cts.cancel();748}749750const location = options?.location ?? model.initialLocation;751const attempt = options?.attempt ?? 0;752const enableCommandDetection = !options?.noCommandDetection;753const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;754755model.removeRequest(request.id, ChatRequestRemovalReason.Resend);756757const resendOptions: IChatSendRequestOptions = {758...options,759locationData: request.locationData,760attachedContext: request.attachedContext,761};762await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise;763}764765async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise<IChatSendRequestData | undefined> {766this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);767768769if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.agentIdSilent) {770this.trace('sendRequest', 'Rejected empty message');771return;772}773774const model = this._sessionModels.get(sessionId);775if (!model) {776throw new Error(`Unknown session: ${sessionId}`);777}778779if (this._pendingRequests.has(sessionId)) {780this.trace('sendRequest', `Session ${sessionId} already has a pending request`);781return;782}783784const requests = model.getRequests();785for (let i = requests.length - 1; i >= 0; i -= 1) {786const request = requests[i];787if (request.shouldBeRemovedOnSend) {788if (request.shouldBeRemovedOnSend.afterUndoStop) {789request.response?.finalizeUndoState();790} else {791await this.removeRequest(sessionId, request.id);792}793}794}795796const location = options?.location ?? model.initialLocation;797const attempt = options?.attempt ?? 0;798const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!;799800const parsedRequest = this.parseChatRequest(sessionId, request, location, options);801const silentAgent = options?.agentIdSilent ? this.chatAgentService.getAgent(options.agentIdSilent) : undefined;802const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent;803const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);804805// This method is only returning whether the request was accepted - don't block on the actual request806return {807...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, silentAgent ?? defaultAgent, location, options),808agent,809slashCommand: agentSlashCommandPart?.command,810};811}812813private parseChatRequest(sessionId: string, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest {814let parserContext = options?.parserContext;815if (options?.agentId) {816const agent = this.chatAgentService.getAgent(options.agentId);817if (!agent) {818throw new Error(`Unknown agent: ${options.agentId}`);819}820parserContext = { selectedAgent: agent, mode: options.modeInfo?.kind };821const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : '';822request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`;823}824825const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext);826return parsedRequest;827}828829private refreshFollowupsCancellationToken(sessionId: string): CancellationToken {830this._sessionFollowupCancelTokens.get(sessionId)?.cancel();831const newTokenSource = new CancellationTokenSource();832this._sessionFollowupCancelTokens.set(sessionId, newTokenSource);833834return newTokenSource.token;835}836837private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState {838const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId);839let request: ChatRequestModel;840const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart);841const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart);842const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart);843const requests = [...model.getRequests()];844const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, {845agentPart,846agentSlashCommandPart,847commandPart,848sessionId: model.sessionId,849location: model.initialLocation,850options,851enableCommandDetection852});853854let gotProgress = false;855const requestType = commandPart ? 'slashCommand' : 'string';856857const responseCreated = new DeferredPromise<IChatResponseModel>();858let responseCreatedComplete = false;859function completeResponseCreated(): void {860if (!responseCreatedComplete && request?.response) {861responseCreated.complete(request.response);862responseCreatedComplete = true;863}864}865866const store = new DisposableStore();867const source = store.add(new CancellationTokenSource());868const token = source.token;869const sendRequestInternal = async () => {870const progressCallback = (progress: IChatProgress[]) => {871if (token.isCancellationRequested) {872return;873}874875gotProgress = true;876877for (let i = 0; i < progress.length; i++) {878const isLast = i === progress.length - 1;879const progressItem = progress[i];880881if (progressItem.kind === 'markdownContent') {882this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progressItem.content.value.length} chars`);883} else {884this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progressItem)}`);885}886887model.acceptResponseProgress(request, progressItem, !isLast);888}889completeResponseCreated();890};891892let detectedAgent: IChatAgentData | undefined;893let detectedCommand: IChatAgentCommand | undefined;894895const stopWatch = new StopWatch(false);896store.add(token.onCancellationRequested(() => {897this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);898if (!request) {899return;900}901902requestTelemetry.complete({903timeToFirstProgress: undefined,904result: 'cancelled',905// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling906totalTime: stopWatch.elapsed(),907requestType,908detectedAgent,909request,910});911912model.cancelRequest(request);913}));914915try {916let rawResult: IChatAgentResult | null | undefined;917let agentOrCommandFollowups: Promise<IChatFollowup[] | undefined> | undefined = undefined;918let chatTitlePromise: Promise<string | undefined> | undefined;919920if (agentPart || (defaultAgent && !commandPart)) {921const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => {922const initVariableData: IChatRequestVariableData = { variables: [] };923request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, options?.modeInfo, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId);924925let variableData: IChatRequestVariableData;926let message: string;927if (chatRequest) {928variableData = chatRequest.variableData;929message = getPromptText(request.message).message;930} else {931variableData = { variables: this.prepareContext(request.attachedContext) };932model.updateRequest(request, variableData);933934const promptTextResult = getPromptText(request.message);935variableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack936message = promptTextResult.message;937}938939let isInitialTools = true;940941store.add(autorun(reader => {942const tools = options?.userSelectedTools?.read(reader);943if (isInitialTools) {944isInitialTools = false;945return;946}947948if (tools) {949this.chatAgentService.setRequestTools(agent.id, request.id, tools);950}951}));952953return {954sessionId,955requestId: request.id,956agentId: agent.id,957message,958command: command?.name,959variables: variableData,960enableCommandDetection,961isParticipantDetected,962attempt,963location,964locationData: request.locationData,965acceptedConfirmationData: options?.acceptedConfirmationData,966rejectedConfirmationData: options?.rejectedConfirmationData,967userSelectedModelId: options?.userSelectedModelId,968userSelectedTools: options?.userSelectedTools?.get(),969modeInstructions: options?.modeInfo?.instructions,970editedFileEvents: request.editedFileEvents971} satisfies IChatAgentRequest;972};973974if (975this.configurationService.getValue('chat.detectParticipant.enabled') !== false &&976this.chatAgentService.hasChatParticipantDetectionProviders() &&977!agentPart &&978!commandPart &&979!agentSlashCommandPart &&980enableCommandDetection &&981options?.modeInfo?.kind !== ChatModeKind.Agent &&982options?.modeInfo?.kind !== ChatModeKind.Edit &&983!options?.agentIdSilent984) {985// We have no agent or command to scope history with, pass the full history to the participant detection provider986const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id);987988// Prepare the request object that we will send to the participant detection provider989const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false);990991const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token);992if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) {993// Update the response in the ChatModel to reflect the detected agent and command994request.response?.setAgent(result.agent, result.command);995detectedAgent = result.agent;996detectedCommand = result.command;997}998}9991000const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!;1001const command = detectedCommand ?? agentSlashCommandPart?.command;1002await Promise.all([1003this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`),1004this.mcpService.autostart(token),1005]);10061007// Recompute history in case the agent or command changed1008const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id);1009const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent);1010const pendingRequest = this._pendingRequests.get(sessionId);1011if (pendingRequest && !pendingRequest.requestId) {1012pendingRequest.requestId = requestProps.requestId;1013}1014completeResponseCreated();1015const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token);1016rawResult = agentResult;1017agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken);1018chatTitlePromise = model.getRequests().length === 1 && !model.customTitle ? this.chatAgentService.getChatTitle(defaultAgent.id, this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id), CancellationToken.None) : undefined;1019} else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) {1020if (commandPart.slashCommand.silent !== true) {1021request = model.addRequest(parsedRequest, { variables: [] }, attempt, options?.modeInfo);1022completeResponseCreated();1023}1024// contributed slash commands1025// TODO: spell this out in the UI1026const history: IChatMessage[] = [];1027for (const modelRequest of model.getRequests()) {1028if (!modelRequest.response) {1029continue;1030}1031history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] });1032history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] });1033}1034const message = parsedRequest.text;1035const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatProgress>(p => {1036progressCallback([p]);1037}), history, location, token);1038agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);1039rawResult = {};10401041} else {1042throw new Error(`Cannot handle request`);1043}10441045if (token.isCancellationRequested && !rawResult) {1046return;1047} else {1048if (!rawResult) {1049this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);1050rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };1051}10521053const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' :1054rawResult.errorDetails && gotProgress ? 'errorWithOutput' :1055rawResult.errorDetails ? 'error' :1056'success';10571058requestTelemetry.complete({1059timeToFirstProgress: rawResult.timings?.firstProgress,1060totalTime: rawResult.timings?.totalElapsed,1061result,1062requestType,1063detectedAgent,1064request,1065});10661067model.setResponse(request, rawResult);1068completeResponseCreated();1069this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);10701071model.completeResponse(request);1072if (agentOrCommandFollowups) {1073agentOrCommandFollowups.then(followups => {1074model.setFollowups(request, followups);1075const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command;1076this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0);1077});1078}1079chatTitlePromise?.then(title => {1080if (title) {1081model.setCustomTitle(title);1082}1083});1084}1085} catch (err) {1086this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`);1087requestTelemetry.complete({1088timeToFirstProgress: undefined,1089totalTime: undefined,1090result: 'error',1091requestType,1092detectedAgent,1093request,1094});1095if (request) {1096const rawResult: IChatAgentResult = { errorDetails: { message: err.message } };1097model.setResponse(request, rawResult);1098completeResponseCreated();1099model.completeResponse(request);1100}1101} finally {1102store.dispose();1103}1104};1105const rawResponsePromise = sendRequestInternal();1106// Note- requestId is not known at this point, assigned later1107this._pendingRequests.set(model.sessionId, this.instantiationService.createInstance(CancellableRequest, source, undefined));1108rawResponsePromise.finally(() => {1109this._pendingRequests.deleteAndDispose(model.sessionId);1110});1111this._onDidSubmitRequest.fire({ chatSessionId: model.sessionId });1112return {1113responseCreatedPromise: responseCreated.p,1114responseCompletePromise: rawResponsePromise,1115};1116}11171118private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] {1119attachedContextVariables ??= [];11201121// "reverse", high index first so that replacement is simple1122attachedContextVariables.sort((a, b) => {1123// If either range is undefined, sort it to the back1124if (!a.range && !b.range) {1125return 0; // Keep relative order if both ranges are undefined1126}1127if (!a.range) {1128return 1; // a goes after b1129}1130if (!b.range) {1131return -1; // a goes before b1132}1133return b.range.start - a.range.start;1134});11351136return attachedContextVariables;1137}11381139private getHistoryEntriesFromModel(requests: IChatRequestModel[], sessionId: string, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] {1140const history: IChatAgentHistoryEntry[] = [];1141const agent = this.chatAgentService.getAgent(forAgentId);1142for (const request of requests) {1143if (!request.response) {1144continue;1145}11461147if (forAgentId !== request.response.agent?.id && !agent?.isDefault) {1148// An agent only gets to see requests that were sent to this agent.1149// The default agent (the undefined case) gets to see all of them.1150continue;1151}11521153const promptTextResult = getPromptText(request.message);1154const historyRequest: IChatAgentRequest = {1155sessionId: sessionId,1156requestId: request.id,1157agentId: request.response.agent?.id ?? '',1158message: promptTextResult.message,1159command: request.response.slashCommand?.name,1160variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack1161location: ChatAgentLocation.Panel,1162editedFileEvents: request.editedFileEvents,1163};1164history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} });1165}11661167return history;1168}11691170async removeRequest(sessionId: string, requestId: string): Promise<void> {1171const model = this._sessionModels.get(sessionId);1172if (!model) {1173throw new Error(`Unknown session: ${sessionId}`);1174}11751176const pendingRequest = this._pendingRequests.get(sessionId);1177if (pendingRequest?.requestId === requestId) {1178pendingRequest.cancel();1179this._pendingRequests.deleteAndDispose(sessionId);1180}11811182model.removeRequest(requestId);1183}11841185async adoptRequest(sessionId: string, request: IChatRequestModel) {1186if (!(request instanceof ChatRequestModel)) {1187throw new TypeError('Can only adopt requests of type ChatRequestModel');1188}1189const target = this._sessionModels.get(sessionId);1190if (!target) {1191throw new Error(`Unknown session: ${sessionId}`);1192}11931194const oldOwner = request.session;1195target.adoptRequest(request);11961197if (request.response && !request.response.isComplete) {1198const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId);1199if (cts) {1200cts.requestId = request.id;1201this._pendingRequests.set(target.sessionId, cts);1202}1203}1204}12051206async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise<void> {1207this.trace('addCompleteRequest', `message: ${message}`);12081209const model = this._sessionModels.get(sessionId);1210if (!model) {1211throw new Error(`Unknown session: ${sessionId}`);1212}12131214const parsedRequest = typeof message === 'string' ?1215this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) :1216message;1217const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0, undefined, undefined, undefined, undefined, undefined, undefined, true);1218if (typeof response.message === 'string') {1219// TODO is this possible?1220model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' });1221} else {1222for (const part of response.message) {1223model.acceptResponseProgress(request, part, true);1224}1225}1226model.setResponse(request, response.result || {});1227if (response.followups !== undefined) {1228model.setFollowups(request, response.followups);1229}1230model.completeResponse(request);1231}12321233cancelCurrentRequestForSession(sessionId: string): void {1234this.trace('cancelCurrentRequestForSession', `sessionId: ${sessionId}`);1235this._pendingRequests.get(sessionId)?.cancel();1236this._pendingRequests.deleteAndDispose(sessionId);1237}12381239async clearSession(sessionId: string): Promise<void> {1240const shouldSaveToHistory = this.shouldSaveToHistory(sessionId);1241this.trace('clearSession', `sessionId: ${sessionId}, save to history: ${shouldSaveToHistory}`);1242const model = this._sessionModels.get(sessionId);1243if (!model) {1244throw new Error(`Unknown session: ${sessionId}`);1245}12461247if (shouldSaveToHistory && (model.initialLocation === ChatAgentLocation.Panel || model.initialLocation === ChatAgentLocation.Editor)) {1248if (this.useFileStorage) {1249// Always preserve sessions that have custom titles, even if empty1250if (model.getRequests().length === 0 && !model.customTitle) {1251await this._chatSessionStore.deleteSession(sessionId);1252} else {1253await this._chatSessionStore.storeSessions([model]);1254}1255} else {1256// Always preserve sessions that have custom titles, even if empty1257if (model.getRequests().length === 0 && !model.customTitle) {1258delete this._persistedSessions[sessionId];1259} else {1260// Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to1261// assign values to properties that are getters- microsoft/vscode-copilot-release#12331262const sessionData: ISerializableChatData = JSON.parse(JSON.stringify(model));1263sessionData.isNew = true;1264this._persistedSessions[sessionId] = sessionData;1265}1266}1267}12681269this._sessionModels.delete(sessionId);1270model.dispose();1271this._pendingRequests.get(sessionId)?.cancel();1272this._pendingRequests.deleteAndDispose(sessionId);1273this._onDidDisposeSession.fire({ sessionId, reason: 'cleared' });1274}12751276public hasSessions(): boolean {1277if (this.useFileStorage) {1278return this._chatSessionStore.hasSessions();1279} else {1280return Object.values(this._persistedSessions).length > 0;1281}1282}12831284transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void {1285const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId);1286if (!model) {1287throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`);1288}12891290const existingRaw: IChatTransfer2[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []);1291existingRaw.push({1292chat: model.toJSON(),1293timestampInMilliseconds: Date.now(),1294toWorkspace: toWorkspace,1295inputValue: transferredSessionData.inputValue,1296location: transferredSessionData.location,1297mode: transferredSessionData.mode,1298});12991300this.storageService.store(globalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE);1301this.chatTransferService.addWorkspaceToTransferred(toWorkspace);1302this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`);1303}13041305getChatStorageFolder(): URI {1306return this._chatSessionStore.getChatStorageFolder();1307}13081309logChatIndex(): void {1310this._chatSessionStore.logIndex();1311}13121313private shouldSaveToHistory(sessionId: string): boolean {1314// We shouldn't save contributed sessions from content providers1315for (const [_, sessions] of this._contentProviderSessionModels) {1316let session: { readonly model: IChatModel; readonly disposables: DisposableStore } | undefined;1317for (const entry of sessions.values()) {1318if (entry.model.sessionId === sessionId) {1319session = entry;1320break;1321}1322}1323if (session) {1324return false;1325}1326}13271328return true;1329}1330}133113321333