Try storing all chat session options in a map

This commit is contained in:
Matt Bierner
2026-03-19 15:15:39 -07:00
parent b4018e320b
commit ea64ce8268
13 changed files with 134 additions and 102 deletions
@@ -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,
});
}
}
@@ -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, {
@@ -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<IChatSessionHistoryItem>;
title?: string;
private _options?: Record<string, string | IChatSessionProviderOptionItem>;
public get options(): Record<string, string | IChatSessionProviderOptionItem> | undefined {
return this._options;
private _options?: ChatSessionOptionsMap;
public get options(): ReadonlyChatSessionOptionsMap | undefined {
return this._options ? new Map(this._options) : undefined;
}
private readonly _progressObservable = observableValue<IChatProgress[]>(this, []);
private readonly _isCompleteObservable = observableValue<boolean>(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<IChatSessionItem | undefined> {
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<string, string | IChatSessionProviderOptionItem>): 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<void> {
@@ -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<void> {
async notifyOptionsChange(handle: number, sessionResource: URI, updates: ReadonlyMap<string, string | IChatSessionProviderOptionItem | undefined>): Promise<void> {
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);
+11 -12
View File
@@ -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<IChatSessionHistoryItemDto, { type: 'request' }>;
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<ChatSessionOptionUpdateDto2>): void;
$onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem>): void;
$onDidChangeChatSessionProviderOptions(handle: number): void;
$handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise<void>;
@@ -3647,7 +3646,7 @@ export interface MainThreadChatSessionsShape extends IDisposable {
export interface ExtHostChatSessionsShape {
$refreshChatSessionItems(providerHandle: number, token: CancellationToken): Promise<void>;
$onDidChangeChatSessionItemState(providerHandle: number, sessionResource: UriComponents, archived: boolean): void;
$newChatSessionItem(controllerHandle: number, request: IChatNewSessionRequest, token: CancellationToken): Promise<Dto<IChatSessionItem> | undefined>;
$newChatSessionItem(controllerHandle: number, request: IChatNewSessionRequestDto, token: CancellationToken): Promise<Dto<IChatSessionItem> | undefined>;
$provideChatSessionContent(providerHandle: number, sessionResource: UriComponents, context: ChatSessionContentContextDto, token: CancellationToken): Promise<ChatSessionDto>;
$interruptChatSessionActiveResponse(providerHandle: number, sessionResource: UriComponents, requestId: string): Promise<void>;
@@ -3655,7 +3654,7 @@ export interface ExtHostChatSessionsShape {
$invokeChatSessionRequestHandler(providerHandle: number, sessionResource: UriComponents, request: IChatAgentRequest, history: any[], token: CancellationToken): Promise<IChatAgentResult>;
$provideChatSessionProviderOptions(providerHandle: number, token: CancellationToken): Promise<IChatSessionProviderOptions | undefined>;
$invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise<IChatSessionProviderOptionItem[]>;
$provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: ReadonlyArray<ChatSessionOptionUpdateDto>, token: CancellationToken): Promise<void>;
$provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem | undefined>, token: CancellationToken): Promise<void>;
$forkChatSession(providerHandle: number, sessionResource: UriComponents, request: IChatSessionRequestHistoryItemDto | undefined, token: CancellationToken): Promise<Dto<IChatSessionItem>>;
}
@@ -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<string, string | IChatSessionProviderOptionItem> = 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<void> {
async $provideHandleOptionsChange(handle: number, sessionResourceComponents: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem | undefined>, token: CancellationToken): Promise<void> {
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<ReturnType<typeof typeConvert.ChatSessionItem.from> | undefined> {
async $newChatSessionItem(handle: number, request: IChatNewSessionRequestDto, token: CancellationToken): Promise<ReturnType<typeof typeConvert.ChatSessionItem.from> | undefined> {
const controllerData = this._chatSessionItemControllers.get(handle);
if (!controllerData) {
this._logService.warn(`No controller found for handle ${handle}`);
@@ -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);
@@ -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<IChatSessionsEx
class ContributedChatSessionData extends Disposable {
private readonly _optionsCache: Map<string /* 'models' */, string | IChatSessionProviderOptionItem>;
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<string, string | IChatSessionProviderOptionItem> | undefined,
readonly options: ReadonlyChatSessionOptionsMap | undefined,
private readonly onWillDispose: (resource: URI) => void
) {
super();
this._optionsCache = new Map<string, string | IChatSessionProviderOptionItem>();
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<string, number> = new Map();
private readonly _sessionTypeOptions: Map<string, IChatSessionProviderOptionGroup[]> = new Map();
private readonly _sessionTypeNewSessionOptions: Map<string, Record<string, string | IChatSessionProviderOptionItem>> = new Map();
private readonly _sessionTypeOptions = new Map<string, IChatSessionProviderOptionGroup[]>();
private readonly _sessionTypeNewSessionOptions = new Map</* sessionType */string, ChatSessionOptionsMap>();
private readonly _sessions = new ResourceMap<ContributedChatSessionData>();
private readonly _resourceAliases = new ResourceMap<URI>(); // 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<string, string> | 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<string, string | IChatSessionProviderOptionItem> | 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<string, string | IChatSessionProviderOptionItem>): 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 = {
@@ -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]])
);
}
}
@@ -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';
@@ -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;
@@ -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<string, string | IChatSessionProviderOptionItem>;
readonly options?: ReadonlyChatSessionOptionsMap;
readonly progressObs?: IObservable<IChatProgress[]>;
readonly isCompleteObs?: IObservable<boolean>;
@@ -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<string, string | IChatSessionProviderOptionItem | undefined>;
}
export type ResolvedChatSessionsExtensionPoint = Omit<IChatSessionsExtensionPoint, 'icon'> & {
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<string, string | IChatSessionProviderOptionItem>;
export namespace ChatSessionOptionsMap {
export function fromRecord(obj: { [key: string]: string | IChatSessionProviderOptionItem }): ChatSessionOptionsMap {
return new Map(Object.entries(obj));
}
export function toRecord(map: ReadonlyChatSessionOptionsMap): Record<string, string | IChatSessionProviderOptionItem> {
const record: Record<string, string | IChatSessionProviderOptionItem> = 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<string, string | IChatSessionProviderOptionItem>;
export const IChatSessionsService = createDecorator<IChatSessionsService>('chatSessionsService');
export interface IChatSessionsService {
@@ -299,10 +330,10 @@ export interface IChatSessionsService {
getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise<IChatSession>;
hasAnySessionOptions(sessionResource: URI): boolean;
getSessionOptions(sessionResource: URI): Map<string, string> | 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<string, string | IChatSessionProviderOptionItem> | undefined;
setNewSessionOptionsForSessionType(chatSessionType: string, options: Record<string, string | IChatSessionProviderOptionItem>): void;
getNewSessionOptionsForSessionType(chatSessionType: string): ReadonlyChatSessionOptionsMap | undefined;
setNewSessionOptionsForSessionType(chatSessionType: string, options: ReadonlyChatSessionOptionsMap): void;
getInProgressSessionDescription(chatModel: IChatModel): string | undefined;
@@ -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' },
@@ -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<string, IChatSessionContentProvider>();
private contributions: IChatSessionsExtensionPoint[] = [];
private optionGroups = new Map<string, IChatSessionProviderOptionGroup[]>();
private sessionOptions = new ResourceMap<Map<string, string>>();
private sessionOptions = new ResourceMap<ChatSessionOptionsMap>();
private inProgress = new Map<string, number>();
// For testing: allow triggering events
@@ -172,33 +172,34 @@ export class MockChatSessionsService implements IChatSessionsService {
}
}
getNewSessionOptionsForSessionType(_chatSessionType: string): Record<string, string | IChatSessionProviderOptionItem> | undefined {
getNewSessionOptionsForSessionType(_chatSessionType: string): ReadonlyChatSessionOptionsMap | undefined {
return undefined;
}
setNewSessionOptionsForSessionType(_chatSessionType: string, _options: Record<string, string | IChatSessionProviderOptionItem>): void {
setNewSessionOptionsForSessionType(_chatSessionType: string, _options: ReadonlyChatSessionOptionsMap): void {
// noop
}
getSessionOptions(sessionResource: URI): Map<string, string> | 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 });