From 8450fc3285e13ee3e0cbe5392afbbd04c32bd64e Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 4 Dec 2025 11:21:56 -0800 Subject: [PATCH 1/6] Ignore errors and UI interactions in fetch tool --- .../electron-main/webPageLoader.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/vs/platform/webContentExtractor/electron-main/webPageLoader.ts b/src/vs/platform/webContentExtractor/electron-main/webPageLoader.ts index 229fe2502ad..c91f67fd44a 100644 --- a/src/vs/platform/webContentExtractor/electron-main/webPageLoader.ts +++ b/src/vs/platform/webContentExtractor/electron-main/webPageLoader.ts @@ -72,6 +72,13 @@ export class WebPageLoader extends Disposable { .once('did-fail-load', this.onFailLoad.bind(this)) .once('will-navigate', this.onRedirect.bind(this)) .once('will-redirect', this.onRedirect.bind(this)); + + // Disable any UI interactions that could interfere with content loading. + this._window.webContents + .on('login', (event) => event.preventDefault()) + .on('select-client-certificate', (event) => event.preventDefault()) + .on('certificate-error', (event) => event.preventDefault()); + } private trace(message: string) { @@ -164,7 +171,12 @@ export class WebPageLoader extends Disposable { } this.trace(`Received 'did-fail-load' event, code: ${statusCode}, error: '${error}'`); - void this._queue.queue(() => this.extractContent({ status: 'error', statusCode, error })); + if (statusCode === -3) { + this.trace(`Ignoring ERR_ABORTED (-3) as it may be caused by CSP or other measures`); + void this._queue.queue(() => this.extractContent()); + } else { + void this._queue.queue(() => this.extractContent({ status: 'error', statusCode, error })); + } } /** From c4c61b3d63f41bedb06b98da8be9c5459e74ce00 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Thu, 4 Dec 2025 11:29:09 -0800 Subject: [PATCH 2/6] Fix unit-tests --- .../test/electron-main/webPageLoader.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/platform/webContentExtractor/test/electron-main/webPageLoader.test.ts b/src/vs/platform/webContentExtractor/test/electron-main/webPageLoader.test.ts index 262e6119be4..8be6d08a3c1 100644 --- a/src/vs/platform/webContentExtractor/test/electron-main/webPageLoader.test.ts +++ b/src/vs/platform/webContentExtractor/test/electron-main/webPageLoader.test.ts @@ -34,6 +34,14 @@ class MockWebContents { return this; } + on(event: string, listener: (...args: unknown[]) => void): this { + if (!this._listeners.has(event)) { + this._listeners.set(event, []); + } + this._listeners.get(event)!.push(listener); + return this; + } + emit(event: string, ...args: unknown[]): void { const listeners = this._listeners.get(event) || []; for (const listener of listeners) { From 6c18678606245c5c3a5c035138852d8d5368455b Mon Sep 17 00:00:00 2001 From: Megan Rogge Date: Thu, 4 Dec 2025 14:09:34 -0600 Subject: [PATCH 3/6] Fix terminal suggest regression, trim ghost text from prompt value before passing to providers (#281327) Fix #281084 --- .../terminalContrib/suggest/browser/terminalSuggestAddon.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 345678b1590..797f0d00dbc 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -306,7 +306,9 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest const quickSuggestionsConfig = this._configurationService.getValue(terminalSuggestConfigSection).quickSuggestions; const allowFallbackCompletions = explicitlyInvoked || quickSuggestionsConfig.unknown === 'on'; this._logService.trace('SuggestAddon#_handleCompletionProviders provideCompletions'); - const providedCompletions = await this._terminalCompletionService.provideCompletions(this._currentPromptInputState.value, this._currentPromptInputState.cursorIndex, allowFallbackCompletions, this.shellType, this._capabilities, token, false, doNotRequestExtensionCompletions, explicitlyInvoked); + // Trim ghost text from the prompt value when requesting completions + const promptValue = this._mostRecentPromptInputState?.ghostTextIndex !== undefined ? this._currentPromptInputState.value.substring(0, this._mostRecentPromptInputState?.ghostTextIndex) : this._currentPromptInputState.value; + const providedCompletions = await this._terminalCompletionService.provideCompletions(promptValue, this._currentPromptInputState.cursorIndex, allowFallbackCompletions, this.shellType, this._capabilities, token, false, doNotRequestExtensionCompletions, explicitlyInvoked); this._logService.trace('SuggestAddon#_handleCompletionProviders provideCompletions done'); if (token.isCancellationRequested) { From 428308c7a966a5c15a5366ceebc673dfe8153353 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 5 Dec 2025 07:24:30 +1100 Subject: [PATCH 4/6] Support triggering complex Chat Session Options (#281324) * Support triggering complex Chat Session Options * Updates * Updates --- .../api/browser/mainThreadChatSessions.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 4 ++-- .../workbench/api/common/extHostChatSessions.ts | 10 +++++++--- .../contrib/chat/browser/chatInputPart.ts | 15 +++++++++++---- .../chat/browser/chatSessions.contribution.ts | 6 +++--- .../contrib/chat/common/chatSessionsService.ts | 6 +++--- .../chat/test/common/mockChatSessionsService.ts | 2 +- .../vscode.proposed.chatSessionsProvider.d.ts | 2 +- 8 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 082d5337828..4c7a459a622 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -342,7 +342,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._proxy = this._extHostContext.getProxy(ExtHostContext.ExtHostChatSessions); - this._chatSessionsService.setOptionsChangeCallback(async (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>) => { + this._chatSessionsService.setOptionsChangeCallback(async (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>) => { const handle = this._getHandleForSessionType(sessionResource.scheme); if (handle !== undefined) { await this.notifyOptionsChange(handle, sessionResource, updates); @@ -629,7 +629,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat /** * Notify the extension about option changes for a session */ - async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>): Promise { + async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>): Promise { try { await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None); } catch (error) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 96832889df9..4a90eb970b9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3275,12 +3275,12 @@ export type IChatSessionHistoryItemDto = { export interface ChatSessionOptionUpdateDto { readonly optionId: string; - readonly value: string | undefined; + readonly value: string | IChatSessionProviderOptionItem | undefined; } export interface ChatSessionOptionUpdateDto2 { readonly optionId: string; - readonly value: string; + readonly value: string | IChatSessionProviderOptionItem; } export interface ChatSessionDto { diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index b25d991e753..7b166295b61 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -15,7 +15,7 @@ import { URI, UriComponents } from '../../../base/common/uri.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; -import { ChatSessionStatus, IChatSessionItem } from '../../contrib/chat/common/chatSessionsService.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; import { ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape } from './extHost.protocol.js'; @@ -309,7 +309,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | undefined }>, token: CancellationToken): Promise { + async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>, token: CancellationToken): Promise { const sessionResource = URI.revive(sessionResourceComponents); const provider = this._chatSessionContentProviders.get(handle); if (!provider) { @@ -323,7 +323,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } try { - await provider.provider.provideHandleOptionsChange(sessionResource, updates, token); + const updatesToSend = updates.map(update => ({ + optionId: update.optionId, + value: update.value === undefined ? undefined : (typeof update.value === 'string' ? update.value : update.value.id) + })); + await provider.provider.provideHandleOptionsChange(sessionResource, updatesToSend, token); } catch (error) { this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionResource ${sessionResource}:`, error); } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 96675ca388c..378fd9469be 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -444,7 +444,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge // React to chat session option changes for the active session this._register(this.chatSessionsService.onDidChangeSessionOptions(e => { const sessionResource = this._widget?.viewModel?.model.sessionResource; - if (sessionResource && isEqual(sessionResource, e.resource)) { + if (sessionResource && isEqual(sessionResource, e)) { // Options changed for our current session - refresh pickers this.refreshChatSessionPickers(); } @@ -710,7 +710,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.getOrCreateOptionEmitter(optionGroup.id).fire(option); this.chatSessionsService.notifySessionOptionsChange( ctx.chatSessionResource, - [{ optionId: optionGroup.id, value: option.id }] + [{ optionId: optionGroup.id, value: option }] ).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); }, getAllOptions: () => { @@ -1270,9 +1270,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (currentOption) { const optionGroup = optionGroups.find(g => g.id === optionGroupId); if (optionGroup) { - const item = optionGroup.items.find(m => m.id === currentOption); + const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + const item = optionGroup.items.find(m => m.id === currentOptionId); if (item) { - this.getOrCreateOptionEmitter(optionGroupId).fire(item); + // If currentOption is an object (not a string ID), it represents a complete option item and should be used directly. + // Otherwise, if it's a string ID, look up the corresponding item and use that. + if (typeof currentOption === 'string') { + this.getOrCreateOptionEmitter(optionGroupId).fire(item); + } else { + this.getOrCreateOptionEmitter(optionGroupId).fire(currentOption); + } } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index cb06bd67998..2be88113c49 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -265,7 +265,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ private readonly _onDidChangeContentProviderSchemes = this._register(new Emitter<{ readonly added: string[]; readonly removed: string[] }>()); public get onDidChangeContentProviderSchemes() { return this._onDidChangeContentProviderSchemes.event; } - private readonly _onDidChangeSessionOptions = this._register(new Emitter<{ readonly resource: URI; readonly updates: ReadonlyArray<{ optionId: string; value: string }> }>()); + private readonly _onDidChangeSessionOptions = this._register(new Emitter()); public get onDidChangeSessionOptions() { return this._onDidChangeSessionOptions.event; } private readonly inProgressMap: Map = new Map(); @@ -1078,7 +1078,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ /** * Notify extension about option changes for a session */ - public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { + public async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise { if (!updates.length) { return; } @@ -1088,7 +1088,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ for (const u of updates) { this.setSessionOption(sessionResource, u.optionId, u.value); } - this._onDidChangeSessionOptions.fire({ resource: sessionResource, updates }); + this._onDidChangeSessionOptions.fire(sessionResource); } /** diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index a2c6c0f3f7e..e5e41b76730 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -150,7 +150,7 @@ export interface IChatSessionContentProvider { export type SessionOptionsChangedCallback = (sessionResource: URI, updates: ReadonlyArray<{ optionId: string; - value: string; + value: string | IChatSessionProviderOptionItem; }>) => Promise; export interface IChatSessionsService { @@ -203,7 +203,7 @@ export interface IChatSessionsService { /** * Fired when options for a chat session change. */ - onDidChangeSessionOptions: Event<{ readonly resource: URI; readonly updates: ReadonlyArray<{ optionId: string; value: string }> }>; + onDidChangeSessionOptions: Event; /** * Get the capabilities for a specific session type @@ -213,7 +213,7 @@ export interface IChatSessionsService { getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void; - notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise; + notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise; // Editable session support setEditableSession(sessionResource: URI, data: IEditableData | null): Promise; diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index fa053835366..1c1238d5080 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -18,7 +18,7 @@ import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessi export class MockChatSessionsService implements IChatSessionsService { _serviceBrand: undefined; - private readonly _onDidChangeSessionOptions = new Emitter<{ readonly resource: URI; readonly updates: ReadonlyArray<{ optionId: string; value: string }> }>(); + private readonly _onDidChangeSessionOptions = new Emitter(); readonly onDidChangeSessionOptions = this._onDidChangeSessionOptions.event; private readonly _onDidChangeItemsProviders = new Emitter(); readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event; diff --git a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts index 217c3db3ca3..7cd69e208e5 100644 --- a/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts @@ -199,7 +199,7 @@ declare module 'vscode' { /** * The new value assigned to the option. When `undefined`, the option is cleared. */ - readonly value: string; + readonly value: string | ChatSessionProviderOptionItem; }>; } From 5eafa95b6d8a456be8db6f1f7ad16ca5ada23648 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:20:11 -0800 Subject: [PATCH 5/6] Debounce change sessions event (#281353) Debounce onDidChangeChatSessionItems --- src/vs/workbench/api/browser/mainThreadChatSessions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 4c7a459a622..32e139405cc 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -5,7 +5,7 @@ import { raceCancellationError } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; -import { Emitter } from '../../../base/common/event.js'; +import { Emitter, Event } from '../../../base/common/event.js'; import { IMarkdownString, MarkdownString } from '../../../base/common/htmlContent.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../base/common/map.js'; @@ -360,7 +360,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat const changeEmitter = disposables.add(new Emitter()); const provider: IChatSessionItemProvider = { chatSessionType, - onDidChangeChatSessionItems: changeEmitter.event, + onDidChangeChatSessionItems: Event.debounce(changeEmitter.event, (_, e) => e, 200), provideChatSessionItems: (token) => this._provideChatSessionItems(handle, token), provideNewChatSessionItem: (options, token) => this._provideNewChatSessionItem(handle, options, token) }; From 92d9126ed1875819b38a340c8026bfc182948faa Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 4 Dec 2025 13:25:01 -0800 Subject: [PATCH 6/6] Store session metadata for external sessions (#281352) * Store session metadata for external sessions Fix #281350 * Tests --- .../api/browser/mainThreadChatSessions.ts | 25 ++++++++-- .../contrib/chat/common/chatService.ts | 1 + .../contrib/chat/common/chatServiceImpl.ts | 28 +++++++++-- .../contrib/chat/common/chatSessionStore.ts | 48 ++++++++++++++++++- .../localAgentSessionsProvider.test.ts | 4 ++ .../chat/test/common/mockChatService.ts | 3 ++ 6 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index 32e139405cc..f4903e774e4 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -16,9 +16,10 @@ import { URI, UriComponents } from '../../../base/common/uri.js'; import { localize } from '../../../nls.js'; import { IDialogService } from '../../../platform/dialogs/common/dialogs.js'; import { ILogService } from '../../../platform/log/common/log.js'; +import { ChatViewPaneTarget, IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js'; import { IChatEditorOptions } from '../../contrib/chat/browser/chatEditor.js'; import { ChatEditorInput } from '../../contrib/chat/browser/chatEditorInput.js'; -import { ChatViewPaneTarget, IChatWidgetService, isIChatViewViewContext } from '../../contrib/chat/browser/chat.js'; +import { awaitStatsForSession } from '../../contrib/chat/common/chat.js'; import { IChatAgentRequest } from '../../contrib/chat/common/chatAgents.js'; import { IChatContentInlineReference, IChatProgress, IChatService } from '../../contrib/chat/common/chatService.js'; import { IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; @@ -448,21 +449,35 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat try { // Get all results as an array from the RPC call const sessions = await this._proxy.$provideChatSessionItems(handle, token); - return sessions.map(session => { + return Promise.all(sessions.map(async session => { const uri = URI.revive(session.resource); const model = this._chatService.getSession(uri); let description: string | undefined; + let statistics: IChatSessionItem['statistics']; if (model) { description = this._chatSessionsService.getSessionDescription(model); } + + const modelStats = model ? + await awaitStatsForSession(model) : + (await this._chatService.getMetadataForSession(uri))?.stats; + if (modelStats) { + statistics = { + files: modelStats.fileCount, + insertions: modelStats.added, + deletions: modelStats.removed + }; + } + return { ...session, resource: uri, iconPath: session.iconPath, tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined, - description: description || session.description - }; - }); + description: description || session.description, + statistics + } satisfies IChatSessionItem; + })); } catch (error) { this._logService.error('Error providing chat sessions:', error); } diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index e6114d5ff0d..903f5640599 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -1053,6 +1053,7 @@ export interface IChatService { logChatIndex(): void; getLiveSessionItems(): Promise; getHistorySessionItems(): Promise; + getMetadataForSession(sessionResource: URI): Promise; readonly onDidPerformUserAction: Event; notifyUserAction(event: IChatUserActionEvent): void; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 00a2d6f81d8..273eb6dcc51 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -39,7 +39,7 @@ import { ChatRequestParser } from './chatRequestParser.js'; import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js'; import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; import { IChatSessionsService } from './chatSessionsService.js'; -import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; +import { ChatSessionStore, IChatSessionEntryMetadata, IChatTransfer2 } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatTransferService } from './chatTransferService.js'; import { LocalChatSessionUri } from './chatUri.js'; @@ -153,6 +153,8 @@ export class ChatService extends Disposable implements IChatService { } else if (this._saveModelsEnabled) { await this._chatSessionStore.storeSessions([model]); } + } else if (!localSessionId && model.getRequests().length > 0) { + await this._chatSessionStore.storeSessionsMetadataOnly([model]); } } })); @@ -217,10 +219,14 @@ export class ChatService extends Disposable implements IChatService { return; } - const liveChats = Array.from(this._sessionModels.values()) + const liveLocalChats = Array.from(this._sessionModels.values()) .filter(session => this.shouldStoreSession(session)); - this._chatSessionStore.storeSessions(liveChats); + this._chatSessionStore.storeSessions(liveLocalChats); + + const liveNonLocalChats = Array.from(this._sessionModels.values()) + .filter(session => !LocalChatSessionUri.parseLocalSessionId(session.sessionResource)); + this._chatSessionStore.storeSessionsMetadataOnly(liveNonLocalChats); } /** @@ -405,18 +411,32 @@ export class ChatService extends Disposable implements IChatService { async getHistorySessionItems(): Promise { const index = await this._chatSessionStore.getIndex(); return Object.values(index) + .filter(entry => !entry.isExternal) .filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && entry.initialLocation === ChatAgentLocation.Chat && !entry.isEmpty) .map((entry): IChatDetail => { const sessionResource = LocalChatSessionUri.forSession(entry.sessionId); return ({ ...entry, sessionResource, - stats: entry.stats, isActive: this._sessionModels.has(sessionResource), }); }); } + async getMetadataForSession(sessionResource: URI): Promise { + const index = await this._chatSessionStore.getIndex(); + const metadata: IChatSessionEntryMetadata | undefined = index[sessionResource.toString()]; + if (metadata) { + return { + ...metadata, + sessionResource, + isActive: this._sessionModels.has(sessionResource), + }; + } + + return undefined; + } + private shouldBeInHistory(entry: ChatModel): boolean { return !entry.isImported && !!LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation === ChatAgentLocation.Chat; } diff --git a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts index f87a4ed5b22..586b7dfa617 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts @@ -24,6 +24,7 @@ import { awaitStatsForSession } from './chat.js'; import { ModifiedFileEntryState } from './chatEditingService.js'; import { ChatModel, IChatModelInputState, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js'; import { IChatSessionStats } from './chatService.js'; +import { LocalChatSessionUri } from './chatUri.js'; import { ChatAgentLocation } from './constants.js'; const maxPersistedSessions = 25; @@ -102,6 +103,27 @@ export class ChatSessionStore extends Disposable { } } + async storeSessionsMetadataOnly(sessions: ChatModel[]): Promise { + if (this.shuttingDown) { + // Don't start this task if we missed the chance to block shutdown + return; + } + + try { + this.storeTask = this.storeQueue.queue(async () => { + try { + await Promise.all(sessions.map(session => this.writeSessionMetadataOnly(session))); + await this.flushIndex(); + } catch (e) { + this.reportError('storeSessions', 'Error storing chat sessions', e); + } + }); + await this.storeTask; + } finally { + this.storeTask = undefined; + } + } + // async storeTransferSession(transferData: IChatTransfer, session: ISerializableChatData): Promise { // try { // const content = JSON.stringify(session, undefined, 2); @@ -144,6 +166,23 @@ export class ChatSessionStore extends Disposable { } } + private async writeSessionMetadataOnly(session: ChatModel): Promise { + // Only to be used for external sessions + if (LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) { + return; + } + + try { + const index = this.internalGetIndex(); + + // TODO get this class on sessionResource + const externalSessionId = session.sessionResource.toString(); + index.entries[externalSessionId] = await getSessionMetadata(session); + } catch (e) { + this.reportError('sessionMetadataWrite', 'Error writing chat session metadata', e); + } + } + private async flushIndex(): Promise { const index = this.internalGetIndex(); try { @@ -163,6 +202,7 @@ export class ChatSessionStore extends Disposable { private async trimEntries(): Promise { const index = this.internalGetIndex(); const entries = Object.entries(index.entries) + .filter(([_id, entry]) => !entry.isExternal) .sort((a, b) => b[1].lastMessageDate - a[1].lastMessageDate) .map(([id]) => id); @@ -400,6 +440,11 @@ export interface IChatSessionEntryMetadata { * filter the old ones out of history. */ isEmpty?: boolean; + + /** + * Whether this session was loaded from an external provider (eg background/cloud sessions). + */ + isExternal?: boolean; } function isChatSessionEntryMetadata(obj: unknown): obj is IChatSessionEntryMetadata { @@ -459,7 +504,8 @@ async function getSessionMetadata(session: ChatModel | ISerializableChatData): P initialLocation: session.initialLocation, hasPendingEdits: session instanceof ChatModel ? (session.editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)) : false, isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0, - stats + stats, + isExternal: session instanceof ChatModel && !LocalChatSessionUri.parseLocalSessionId(session.sessionResource) }; } diff --git a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts index ed994224f27..5764ed86c21 100644 --- a/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/localAgentSessionsProvider.test.ts @@ -181,6 +181,10 @@ class MockChatService implements IChatService { waitForModelDisposals(): Promise { return Promise.resolve(); } + + getMetadataForSession(sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } } function createMockChatModel(options: { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 64034a1dfd2..c3ba024a3d0 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -146,4 +146,7 @@ export class MockChatService implements IChatService { waitForModelDisposals(): Promise { throw new Error('Method not implemented.'); } + getMetadataForSession(sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } }