diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts index bbd1cd22998..1a5a8810b33 100644 --- a/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts +++ b/src/vs/sessions/contrib/sessions/browser/sessionsManagementService.ts @@ -401,12 +401,9 @@ export class SessionsManagementService extends Disposable implements ISessionsMa if (selectedOptions && selectedOptions.size > 0) { const contributedSession = model.contributedChatSession; if (contributedSession) { - const initialSessionOptions = [...selectedOptions.entries()].map( - ([optionId, value]) => ({ optionId, value }) - ); model.setContributedChatSession({ ...contributedSession, - initialSessionOptions, + initialSessionOptions: selectedOptions, }); } } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index f40d952f332..8ac37889384 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -34,7 +34,7 @@ import { ChatRequestAgentPart } from '../../contrib/chat/common/requestParser/ch import { ChatRequestParser } from '../../contrib/chat/common/requestParser/chatRequestParser.js'; import { getDynamicVariablesForWidget, getSelectedToolAndToolSetsForWidget } from '../../contrib/chat/browser/attachments/chatVariables.js'; import { IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatService, IChatTask, IChatTaskSerialized, IChatWarningMessage } from '../../contrib/chat/common/chatService/chatService.js'; -import { IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; +import { ChatSessionOptionsMap, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../contrib/chat/common/constants.js'; import { ILanguageModelToolsService } from '../../contrib/chat/common/tools/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; @@ -253,10 +253,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA chatSessionContext = { chatSessionResource, isUntitled, - initialSessionOptions: contributedSession.initialSessionOptions?.map(o => ({ - optionId: o.optionId, - value: typeof o.value === 'string' ? o.value : o.value.id, - })), + initialSessionOptions: ChatSessionOptionsMap.toStrValueArray(contributedSession.initialSessionOptions), }; } return await this._proxy.$invokeAgent(handle, request, { diff --git a/src/vs/workbench/api/browser/mainThreadChatSessions.ts b/src/vs/workbench/api/browser/mainThreadChatSessions.ts index fef8a35d419..8bf7c55d481 100644 --- a/src/vs/workbench/api/browser/mainThreadChatSessions.ts +++ b/src/vs/workbench/api/browser/mainThreadChatSessions.ts @@ -25,7 +25,7 @@ import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/c import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; import { awaitStatsForSession } from '../../contrib/chat/common/chat.js'; import { IChatContentInlineReference, IChatProgress, IChatService, ResponseModelState } from '../../contrib/chat/common/chatService/chatService.js'; -import { ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsService } from '../../contrib/chat/common/chatSessionsService.js'; +import { ChatSessionOptionsMap, ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsService, ReadonlyChatSessionOptionsMap } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatModel } from '../../contrib/chat/common/model/chatModel.js'; import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js'; @@ -44,9 +44,9 @@ export class ObservableChatSession extends Disposable implements IChatSession { readonly providerHandle: number; readonly history: Array; title?: string; - private _options?: Record; - public get options(): Record | undefined { - return this._options; + private _options?: ChatSessionOptionsMap; + public get options(): ReadonlyChatSessionOptionsMap | undefined { + return this._options ? new Map(this._options) : undefined; } private readonly _progressObservable = observableValue(this, []); private readonly _isCompleteObservable = observableValue(this, false); @@ -115,7 +115,7 @@ export class ObservableChatSession extends Disposable implements IChatSession { token ); - this._options = sessionContent.options; + this._options = sessionContent.options ? ChatSessionOptionsMap.fromRecord(sessionContent.options) : undefined; this.title = sessionContent.title; this.history.length = 0; this.history.push(...sessionContent.history.map((turn: IChatSessionHistoryItemDto) => { @@ -383,7 +383,11 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes } async newChatSessionItem(request: IChatNewSessionRequest, token: CancellationToken): Promise { - const dto = await raceCancellationError(this._proxy.$newChatSessionItem(this._handle, request, token), token); + const dto = await raceCancellationError(this._proxy.$newChatSessionItem(this._handle, { + prompt: request.prompt, + command: request.command, + initialSessionOptions: request.initialSessionOptions ? ChatSessionOptionsMap.toStrValueArray(request.initialSessionOptions) : undefined, + }, token), token); if (!dto) { return undefined; } @@ -458,7 +462,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._register(this._chatSessionsService.onDidChangeSessionOptions(({ sessionResource, updates }) => { warnOnUntitledSessionResource(sessionResource, this._logService); const handle = this._getHandleForSessionType(sessionResource.scheme); - this._logService.trace(`[MainThreadChatSessions] onRequestNotifyExtension received: scheme '${sessionResource.scheme}', handle ${handle}, ${updates.length} update(s)`); + this._logService.trace(`[MainThreadChatSessions] onRequestNotifyExtension received: scheme '${sessionResource.scheme}', handle ${handle}, ${updates.size} update(s)`); if (handle !== undefined) { this.notifyOptionsChange(handle, sessionResource, updates); } else { @@ -546,10 +550,10 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat controller.addOrUpdateItem(resolvedItem); } - $onDidChangeChatSessionOptions(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string }>): void { + $onDidChangeChatSessionOptions(handle: number, sessionResourceComponents: UriComponents, updates: Record): void { const sessionResource = URI.revive(sessionResourceComponents); warnOnUntitledSessionResource(sessionResource, this._logService); - this._chatSessionsService.updateSessionOptions(sessionResource, updates); + this._chatSessionsService.updateSessionOptions(sessionResource, ChatSessionOptionsMap.fromRecord(updates)); } async $onDidCommitChatSessionItem(handle: number, originalComponents: UriComponents, modifiedCompoennts: UriComponents): Promise { @@ -696,12 +700,12 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat try { const initialSessionOptions = this._chatSessionsService.getSessionOptions(sessionResource); await session.initialize(token, { - initialSessionOptions: initialSessionOptions ? [...initialSessionOptions].map(([optionId, value]) => ({ optionId, value })) : undefined, + initialSessionOptions: initialSessionOptions ? [...initialSessionOptions].map(([optionId, value]) => ({ optionId, value: typeof value === 'string' ? value : value?.id })) : undefined, }); if (session.options) { for (const [_, handle] of this._sessionTypeToHandle) { if (handle === providerHandle) { - for (const [optionId, value] of Object.entries(session.options)) { + for (const [optionId, value] of session.options) { this._chatSessionsService.setSessionOption(sessionResource, optionId, value); } break; @@ -810,7 +814,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, groupsWithCallbacks); } if (options?.newSessionOptions) { - this._chatSessionsService.setNewSessionOptionsForSessionType(chatSessionScheme, options.newSessionOptions); + this._chatSessionsService.setNewSessionOptionsForSessionType(chatSessionScheme, ChatSessionOptionsMap.fromRecord(options.newSessionOptions)); } }).catch(err => this._logService.error('Error fetching chat session options', err)); } @@ -850,10 +854,10 @@ 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 | IChatSessionProviderOptionItem | undefined }>): Promise { + async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyMap): Promise { this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: starting proxy call for handle ${handle}, sessionResource ${sessionResource}`); try { - await this._proxy.$provideHandleOptionsChange(handle, sessionResource, updates, CancellationToken.None); + await this._proxy.$provideHandleOptionsChange(handle, sessionResource, Object.fromEntries(updates), CancellationToken.None); this._logService.trace(`[MainThreadChatSessions] notifyOptionsChange: proxy call completed for handle ${handle}, sessionResource ${sessionResource}`); } catch (error) { this._logService.error(`[MainThreadChatSessions] notifyOptionsChange: error for handle ${handle}, sessionResource ${sessionResource}:`, error); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7972ca1fb01..5c2be45087c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -60,7 +60,7 @@ import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common import { IChatContextItem } from '../../contrib/chat/common/contextContrib/chatContext.js'; import { IChatProgressHistoryResponseContent, IChatRequestVariableData } from '../../contrib/chat/common/model/chatModel.js'; import { ChatResponseClearToPreviousToolInvocationReason, IChatContentInlineReference, IChatExternalEditsDto, IChatFollowup, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService/chatService.js'; -import { IChatNewSessionRequest, IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js'; +import { IChatSessionItem, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js'; import { IChatRequestVariableValue } from '../../contrib/chat/common/attachments/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponsePart, ILanguageModelChatInfoOptions, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatRequestOptions, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js'; @@ -3592,20 +3592,19 @@ export type IChatSessionHistoryItemDto = { export type IChatSessionRequestHistoryItemDto = Extract; -export interface ChatSessionOptionUpdateDto { - readonly optionId: string; - readonly value: string | IChatSessionProviderOptionItem | undefined; -} -export interface ChatSessionOptionUpdateDto2 { - readonly optionId: string; - readonly value: string | IChatSessionProviderOptionItem; -} export interface ChatSessionContentContextDto { readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string }>; } +export interface IChatNewSessionRequestDto { + readonly prompt: string; + readonly command?: string; + + readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string }>; +} + export interface ChatSessionDto { id: string; resource: UriComponents; @@ -3636,7 +3635,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { $onDidCommitChatSessionItem(controllerHandle: number, original: UriComponents, modified: UriComponents): void; $registerChatSessionContentProvider(handle: number, chatSessionScheme: string): void; $unregisterChatSessionContentProvider(handle: number): void; - $onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: ReadonlyArray): void; + $onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: Record): void; $onDidChangeChatSessionProviderOptions(handle: number): void; $handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise; @@ -3647,7 +3646,7 @@ export interface MainThreadChatSessionsShape extends IDisposable { export interface ExtHostChatSessionsShape { $refreshChatSessionItems(providerHandle: number, token: CancellationToken): Promise; $onDidChangeChatSessionItemState(providerHandle: number, sessionResource: UriComponents, archived: boolean): void; - $newChatSessionItem(controllerHandle: number, request: IChatNewSessionRequest, token: CancellationToken): Promise | undefined>; + $newChatSessionItem(controllerHandle: number, request: IChatNewSessionRequestDto, token: CancellationToken): Promise | undefined>; $provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, context: ChatSessionContentContextDto, token: CancellationToken): Promise; $interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise; @@ -3655,7 +3654,7 @@ export interface ExtHostChatSessionsShape { $invokeChatSessionRequestHandler(providerHandle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise; $provideChatSessionProviderOptions(providerHandle: number, token: CancellationToken): Promise; $invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise; - $provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: ReadonlyArray, token: CancellationToken): Promise; + $provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: Record, token: CancellationToken): Promise; $forkChatSession(providerHandle: number, sessionResource: UriComponents, request: IChatSessionRequestHistoryItemDto | undefined, token: CancellationToken): Promise>; } diff --git a/src/vs/workbench/api/common/extHostChatSessions.ts b/src/vs/workbench/api/common/extHostChatSessions.ts index 9ecd3a3a6e4..81d57e5ee1d 100644 --- a/src/vs/workbench/api/common/extHostChatSessions.ts +++ b/src/vs/workbench/api/common/extHostChatSessions.ts @@ -18,11 +18,11 @@ import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ILogService } from '../../../platform/log/common/log.js'; import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, ISymbolVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js'; -import { IChatNewSessionRequest, IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js'; +import { IChatSessionProviderOptionItem } from '../../contrib/chat/common/chatSessionsService.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/participants/chatAgents.js'; import { Proxied } from '../../services/extensions/common/proxyIdentifier.js'; -import { ChatSessionContentContextDto, ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionRequestHistoryItemDto, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape } from './extHost.protocol.js'; +import { ChatSessionContentContextDto, ChatSessionDto, ExtHostChatSessionsShape, IChatAgentProgressShape, IChatSessionRequestHistoryItemDto, IChatSessionProviderOptions, MainContext, MainThreadChatSessionsShape, IChatNewSessionRequestDto } from './extHost.protocol.js'; import { ChatAgentResponseStream } from './extHostChatAgents2.js'; import { CommandsConverter, ExtHostCommands } from './extHostCommands.js'; import { ExtHostLanguageModels } from './extHostLanguageModels.js'; @@ -485,7 +485,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio if (provider.onDidChangeChatSessionOptions) { disposables.add(provider.onDidChangeChatSessionOptions(evt => { - this._proxy.$onDidChangeChatSessionOptions(handle, evt.resource, evt.updates); + const updates: Record = Object.create(null); + for (const update of evt.updates) { + updates[update.optionId] = update.value; + } + this._proxy.$onDidChangeChatSessionOptions(handle, evt.resource, updates); })); } @@ -568,7 +572,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio }; } - async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem | undefined }>, token: CancellationToken): Promise { + async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: Record, token: CancellationToken): Promise { const sessionResource = URI.revive(sessionResourceComponents); const provider = this._chatSessionContentProviders.get(handle); if (!provider) { @@ -582,11 +586,11 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio } try { - const updatesToSend = updates.map(update => ({ - optionId: update.optionId, - value: update.value === undefined ? undefined : (typeof update.value === 'string' ? update.value : update.value.id) + const updatesToSend = Object.entries(updates).map(([optionId, value]) => ({ + optionId, + value: value === undefined ? undefined : (typeof value === 'string' ? value : value.id) })); - await provider.provider.provideHandleOptionsChange(sessionResource, updatesToSend, token); + provider.provider.provideHandleOptionsChange(sessionResource, updatesToSend, token); } catch (error) { this._logService.error(`Error calling provideHandleOptionsChange for handle ${handle}, sessionResource ${sessionResource}:`, error); } @@ -831,7 +835,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio await controllerData.controller.refreshHandler(token); } - async $newChatSessionItem(handle: number, request: IChatNewSessionRequest, token: CancellationToken): Promise | undefined> { + async $newChatSessionItem(handle: number, request: IChatNewSessionRequestDto, token: CancellationToken): Promise | undefined> { const controllerData = this._chatSessionItemControllers.get(handle); if (!controllerData) { this._logService.warn(`No controller found for handle ${handle}`); diff --git a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts index 255313c43b5..ffbce98d40f 100644 --- a/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts +++ b/src/vs/workbench/api/test/browser/mainThreadChatSessions.test.ts @@ -34,7 +34,7 @@ import { ExtHostChatSessions } from '../../common/extHostChatSessions.js'; import { ExtHostCommands } from '../../common/extHostCommands.js'; import { ExtHostLanguageModels } from '../../common/extHostLanguageModels.js'; import * as extHostTypes from '../../common/extHostTypes.js'; -import { ExtHostChatSessionsShape, IChatProgressDto, IChatSessionProviderOptions } from '../../common/extHost.protocol.js'; +import { ChatSessionDto, ExtHostChatSessionsShape, IChatProgressDto, IChatSessionProviderOptions } from '../../common/extHost.protocol.js'; import { IExtHostAuthentication } from '../../common/extHostAuthentication.js'; import { IExtHostTelemetry } from '../../common/extHostTelemetry.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; @@ -744,11 +744,14 @@ suite('MainThreadChatSessions', function () { mainThread.$registerChatSessionContentProvider(handle, sessionScheme); - const sessionContent = { + const sessionContent: ChatSessionDto = { id: 'test-session', + resource: URI.parse(`${sessionScheme}:/test-session`), history: [], hasActiveResponseCallback: false, hasRequestHandler: false, + hasForkHandler: false, + supportsInterruption: false, options: { 'models': 'gpt-4' } @@ -770,7 +773,7 @@ suite('MainThreadChatSessions', function () { const call = (proxy.$provideHandleOptionsChange as sinon.SinonStub).firstCall; assert.strictEqual(call.args[0], handle); assert.deepStrictEqual(call.args[1], resource); - assert.deepStrictEqual(call.args[2], [{ optionId: 'models', value: 'gpt-4-turbo' }]); + assert.deepStrictEqual(call.args[2], { models: 'gpt-4-turbo' }); mainThread.$unregisterChatSessionContentProvider(handle); }); @@ -787,9 +790,9 @@ suite('MainThreadChatSessions', function () { // Attempt to notify option change for an unregistered scheme // This should not throw, but also should not call the proxy - chatSessionsService.updateSessionOptions(resource, [ - { optionId: 'models', value: 'gpt-4-turbo' } - ]); + chatSessionsService.updateSessionOptions(resource, new Map([ + ['models', 'gpt-4-turbo'] + ])); // Verify the extension was NOT notified (no provider registered) assert.strictEqual((proxy.$provideHandleOptionsChange as sinon.SinonStub).callCount, 0); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts index fb12d5a1d04..f98c6d170b6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts @@ -31,7 +31,7 @@ import { ExtensionsRegistry } from '../../../../services/extensions/common/exten import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; -import { IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionOptionsChangeEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; +import { ChatSessionOptionsMap, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionOptionsChangeEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, ReadonlyChatSessionOptionsMap, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { CHAT_CATEGORY } from '../actions/chatActions.js'; import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; @@ -235,7 +235,7 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint; + private readonly _optionsCache: ChatSessionOptionsMap; public getOption(optionId: string): string | IChatSessionProviderOptionItem | undefined { return this._optionsCache.get(optionId); } @@ -250,17 +250,12 @@ class ContributedChatSessionData extends Disposable { readonly session: IChatSession, readonly chatSessionType: string, readonly resource: URI, - readonly options: Record | undefined, + readonly options: ReadonlyChatSessionOptionsMap | undefined, private readonly onWillDispose: (resource: URI) => void ) { super(); - this._optionsCache = new Map(); - if (options) { - for (const [key, value] of Object.entries(options)) { - this._optionsCache.set(key, value); - } - } + this._optionsCache = new Map(options); this._register(this.session.onWillDispose(() => { this.onWillDispose(this.resource); @@ -301,8 +296,8 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public get onDidChangeOptionGroups() { return this._onDidChangeOptionGroups.event; } private readonly inProgressMap: Map = new Map(); - private readonly _sessionTypeOptions: Map = new Map(); - private readonly _sessionTypeNewSessionOptions: Map> = new Map(); + private readonly _sessionTypeOptions = new Map(); + private readonly _sessionTypeNewSessionOptions = new Map(); private readonly _sessions = new ResourceMap(); private readonly _resourceAliases = new ResourceMap(); // real resource -> untitled resource @@ -1073,8 +1068,10 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ session = await raceCancellationError(provider.provideChatSessionContent(sessionResource, token), token); } - for (const [optionId, value] of Object.entries(session.options ?? {})) { - this.setSessionOption(sessionResource, optionId, value); + if (session.options) { + for (const [optionId, value] of session.options) { + this.setSessionOption(sessionResource, optionId, value); + } } // Make sure another session wasn't created while we were awaiting the provider @@ -1095,8 +1092,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ // Make sure any listeners are aware of the new session and its options if (session.options) { - const updates = Object.entries(session.options).map(([optionId, value]) => ({ optionId, value })); - this._onDidChangeSessionOptions.fire({ sessionResource, updates }); + this._onDidChangeSessionOptions.fire({ sessionResource, updates: session.options }); } return session; @@ -1104,7 +1100,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ public hasAnySessionOptions(sessionResource: URI): boolean { const session = this._sessions.get(this._resolveResource(sessionResource)); - return !!session && !!session.options && Object.keys(session.options).length > 0; + return !!session && !!session.options && session.options.size > 0; } public getSessionOptions(sessionResource: URI): Map | undefined { @@ -1125,17 +1121,17 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ } public setSessionOption(sessionResource: URI, optionId: string, value: string | IChatSessionProviderOptionItem): boolean { - return this.updateSessionOptions(sessionResource, [{ optionId, value }]); + return this.updateSessionOptions(sessionResource, new Map([[optionId, value]])); } - public updateSessionOptions(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): boolean { + public updateSessionOptions(sessionResource: URI, updates: ReadonlyChatSessionOptionsMap): boolean { const session = this._sessions.get(this._resolveResource(sessionResource)); if (!session) { return false; } let didChange = false; - for (const { optionId, value } of updates) { + for (const [optionId, value] of updates) { const existingValue = session.getOption(optionId); if (existingValue !== value) { session.setOption(optionId, value); @@ -1181,12 +1177,12 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ return this._sessionTypeOptions.get(chatSessionType); } - public getNewSessionOptionsForSessionType(chatSessionType: string): Record | undefined { - return this._sessionTypeNewSessionOptions.get(chatSessionType); + public getNewSessionOptionsForSessionType(chatSessionType: string): ReadonlyChatSessionOptionsMap | undefined { + return new Map(this._sessionTypeNewSessionOptions.get(chatSessionType)); } - public setNewSessionOptionsForSessionType(chatSessionType: string, options: Record): void { - this._sessionTypeNewSessionOptions.set(chatSessionType, options); + public setNewSessionOptionsForSessionType(chatSessionType: string, options: ReadonlyChatSessionOptionsMap): void { + this._sessionTypeNewSessionOptions.set(chatSessionType, new Map(options)); } /** @@ -1293,7 +1289,7 @@ export enum ChatSessionPosition { type NewChatSessionSendOptions = { readonly prompt: string; readonly attachedContext?: IChatRequestVariableEntry[]; - readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | { id: string; name: string } }>; + readonly initialSessionOptions?: ReadonlyChatSessionOptionsMap; }; export type NewChatSessionOpenOptions = { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 37dbe0bfed8..55a40c0a6a2 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -706,7 +706,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (needsUpdate) { this.chatSessionsService.updateSessionOptions( ctx.chatSessionResource, - [{ optionId: agentOptionId, value: mode.isBuiltin ? '' : modeName }] + new Map([[agentOptionId, mode.isBuiltin ? '' : modeName]]) ); } } diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts index f63c853a48b..8e7dc8dc246 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -31,6 +31,7 @@ import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js import { IChatRequestVariableValue } from '../attachments/chatVariables.js'; import { ChatAgentLocation } from '../constants.js'; import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from '../tools/languageModelToolsService.js'; +import { ReadonlyChatSessionOptionsMap } from '../chatSessionsService.js'; export interface IChatRequest { message: string; @@ -1541,7 +1542,7 @@ export interface IChatService { export interface IChatSessionContext { readonly chatSessionResource: URI; - readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | { id: string; name: string } }>; + readonly initialSessionOptions?: ReadonlyChatSessionOptionsMap; } export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts index a1d839b05b0..6aea62fe4eb 100644 --- a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -830,8 +830,7 @@ export class ChatService extends Disposable implements IChatService { // Capture session options before loading the remote session, // since the alias registration below may change the lookup. - const sessionOptions = this.chatSessionService.getSessionOptions(sessionResource); - const initialSessionOptions = sessionOptions ? [...sessionOptions].map(([optionId, value]) => ({ optionId, value })) : undefined; + const initialSessionOptions = this.chatSessionService.getSessionOptions(sessionResource); const newItem = await this.chatSessionService.createNewChatSessionItem(getChatSessionType(sessionResource), { prompt: requestText, command: commandPart?.text, initialSessionOptions }, CancellationToken.None); if (newItem) { @@ -847,7 +846,7 @@ export class ChatService extends Disposable implements IChatService { // so that the agent receives them when invoked. model.setContributedChatSession({ chatSessionResource: newItem.resource, - initialSessionOptions: sessionOptions ? [...sessionOptions].map(([optionId, value]) => ({ optionId, value })) : undefined, + initialSessionOptions: initialSessionOptions, }); sessionResource = newItem.resource; diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 6401409f2b9..f69e3ffb662 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -174,11 +174,8 @@ export interface IChatSession extends IDisposable { readonly history: readonly IChatSessionHistoryItem[]; - /** - * Session options as key-value pairs. Keys correspond to option group IDs (e.g., 'models', 'subagents') - * and values are either the selected option item IDs (string) or full option items (for locked state). - */ - readonly options?: Record; + + readonly options?: ReadonlyChatSessionOptionsMap; readonly progressObs?: IObservable; readonly isCompleteObs?: IObservable; @@ -217,7 +214,7 @@ export interface IChatNewSessionRequest { readonly prompt: string; readonly command?: string; - readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>; + readonly initialSessionOptions?: ReadonlyChatSessionOptionsMap; } export interface IChatSessionItemsDelta { @@ -238,13 +235,47 @@ export interface IChatSessionItemController { export interface IChatSessionOptionsChangeEvent { readonly sessionResource: URI; - readonly updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>; + readonly updates: ReadonlyMap; } export type ResolvedChatSessionsExtensionPoint = Omit & { readonly icon: ThemeIcon | URI | undefined; }; +/** + * Session options as key-value pairs. + * + * Keys correspond to option group IDs (e.g., 'models', 'subagents') and values are either the selected option item IDs (string) or full option items (for locked state). + */ +export type ChatSessionOptionsMap = Map; + +export namespace ChatSessionOptionsMap { + export function fromRecord(obj: { [key: string]: string | IChatSessionProviderOptionItem }): ChatSessionOptionsMap { + return new Map(Object.entries(obj)); + } + + export function toRecord(map: ReadonlyChatSessionOptionsMap): Record { + const record: Record = Object.create(null); + for (const [key, value] of map) { + record[key] = value; + } + return record; + } + + export function toStrValueArray(map: ReadonlyChatSessionOptionsMap | undefined): Array<{ optionId: string; value: string }> | undefined { + if (!map) { + return undefined; + } + return Array.from(map, ([optionId, value]) => ({ optionId, value: typeof value === 'string' ? value : value.id })); + } +} + +/** + * Readonly version of {@link ChatSessionOptionsMap} + */ +export type ReadonlyChatSessionOptionsMap = ReadonlyMap; + + export const IChatSessionsService = createDecorator('chatSessionsService'); export interface IChatSessionsService { @@ -299,10 +330,10 @@ export interface IChatSessionsService { getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise; hasAnySessionOptions(sessionResource: URI): boolean; - getSessionOptions(sessionResource: URI): Map | undefined; + getSessionOptions(sessionResource: URI): ReadonlyChatSessionOptionsMap | undefined; getSessionOption(sessionResource: URI, optionId: string): string | IChatSessionProviderOptionItem | undefined; setSessionOption(sessionResource: URI, optionId: string, value: string | IChatSessionProviderOptionItem): boolean; - updateSessionOptions(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): boolean; + updateSessionOptions(sessionResource: URI, updates: ReadonlyChatSessionOptionsMap): boolean; /** * Fired when options for a chat session change. @@ -350,8 +381,8 @@ export interface IChatSessionsService { getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; - getNewSessionOptionsForSessionType(chatSessionType: string): Record | undefined; - setNewSessionOptionsForSessionType(chatSessionType: string, options: Record): void; + getNewSessionOptionsForSessionType(chatSessionType: string): ReadonlyChatSessionOptionsMap | undefined; + setNewSessionOptionsForSessionType(chatSessionType: string, options: ReadonlyChatSessionOptionsMap): void; getInProgressSessionDescription(chatModel: IChatModel): string | undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts index 6727b376b48..c4dacb71de5 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -55,7 +55,7 @@ import { MockChatVariablesService } from '../mockChatVariables.js'; import { MockPromptsService } from '../promptSyntax/service/mockPromptsService.js'; import { MockLanguageModelToolsService } from '../tools/mockLanguageModelToolsService.js'; import { MockChatService } from './mockChatService.js'; -import { IChatSessionsService } from '../../../common/chatSessionsService.js'; +import { ChatSessionOptionsMap, IChatSessionsService } from '../../../common/chatSessionsService.js'; import { MockChatSessionsService } from '../mockChatSessionsService.js'; const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; @@ -937,7 +937,7 @@ suite('ChatService', () => { assert.ok(newModel, 'New model should exist at the real resource'); assert.ok(newModel.contributedChatSession, 'New model should have contributedChatSession'); assert.deepStrictEqual( - newModel.contributedChatSession?.initialSessionOptions?.map(o => ({ optionId: o.optionId, value: o.value })), + ChatSessionOptionsMap.toStrValueArray(newModel.contributedChatSession?.initialSessionOptions), [ { optionId: 'model', value: 'claude-3.5-sonnet' }, { optionId: 'repo', value: 'my-repo' }, diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index 1d35d055d21..c2eb2fef88a 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -9,7 +9,7 @@ import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionOptionsChangeEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsExtensionPoint, IChatSessionsService, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js'; +import { ReadonlyChatSessionOptionsMap, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionOptionsChangeEvent, IChatSessionProviderOptionGroup, IChatSessionRequestHistoryItem, IChatSessionsExtensionPoint, IChatSessionsService, ResolvedChatSessionsExtensionPoint, ChatSessionOptionsMap } from '../../common/chatSessionsService.js'; import { IChatModel } from '../../common/model/chatModel.js'; import { IChatAgentAttachmentCapabilities } from '../../common/participants/chatAgents.js'; import { Target } from '../../common/promptSyntax/promptTypes.js'; @@ -43,7 +43,7 @@ export class MockChatSessionsService implements IChatSessionsService { private contentProviders = new Map(); private contributions: IChatSessionsExtensionPoint[] = []; private optionGroups = new Map(); - private sessionOptions = new ResourceMap>(); + private sessionOptions = new ResourceMap(); private inProgress = new Map(); // For testing: allow triggering events @@ -172,33 +172,34 @@ export class MockChatSessionsService implements IChatSessionsService { } } - getNewSessionOptionsForSessionType(_chatSessionType: string): Record | undefined { + getNewSessionOptionsForSessionType(_chatSessionType: string): ReadonlyChatSessionOptionsMap | undefined { return undefined; } - setNewSessionOptionsForSessionType(_chatSessionType: string, _options: Record): void { + setNewSessionOptionsForSessionType(_chatSessionType: string, _options: ReadonlyChatSessionOptionsMap): void { // noop } - getSessionOptions(sessionResource: URI): Map | undefined { + getSessionOptions(sessionResource: URI): ReadonlyChatSessionOptionsMap | undefined { const options = this.sessionOptions.get(sessionResource); return options && options.size > 0 ? options : undefined; } getSessionOption(sessionResource: URI, optionId: string): string | undefined { - return this.sessionOptions.get(sessionResource)?.get(optionId); + const value = this.sessionOptions.get(sessionResource)?.get(optionId); + return typeof value === 'string' ? value : value?.id; } setSessionOption(sessionResource: URI, optionId: string, value: string): boolean { - return this.updateSessionOptions(sessionResource, [{ optionId, value }]); + return this.updateSessionOptions(sessionResource, new Map([[optionId, value]])); } - updateSessionOptions(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): boolean { + updateSessionOptions(sessionResource: URI, updates: ReadonlyChatSessionOptionsMap): boolean { if (!this.sessionOptions.has(sessionResource)) { this.sessionOptions.set(sessionResource, new Map()); } - for (const update of updates) { - this.sessionOptions.get(sessionResource)!.set(update.optionId, update.value); + for (const [optionId, value] of updates) { + this.sessionOptions.get(sessionResource)!.set(optionId, value); } this._onDidChangeSessionOptions.fire({ sessionResource, updates });