From 73a9d49dea46f46484e84964b2d3fafa88fc257d Mon Sep 17 00:00:00 2001 From: Harald Kirschner Date: Thu, 12 Feb 2026 16:03:46 -0800 Subject: [PATCH] Merge pull request #294779 from microsoft/digitarald/explore-agent-default-model Add Explore agent default model picker and refactor DefaultModelContribution --- .../contrib/chat/browser/chat.contribution.ts | 9 ++ .../chat/browser/defaultModelContribution.ts | 137 ++++++++++++++++++ .../chat/browser/exploreAgentDefaultModel.ts | 34 +++++ .../chat/browser/planAgentDefaultModel.ts | 103 ++----------- .../contrib/chat/common/constants.ts | 1 + .../browser/inlineChatDefaultModel.ts | 108 +++----------- 6 files changed, 214 insertions(+), 178 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/defaultModelContribution.ts create mode 100644 src/vs/workbench/contrib/chat/browser/exploreAgentDefaultModel.ts diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index ff1604d72b9..035ee4ffe08 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -143,6 +143,7 @@ import { ChatRepoInfoContribution } from './chatRepoInfo.js'; import { VALID_PROMPT_FOLDER_PATTERN } from '../common/promptSyntax/utils/promptFilesLocator.js'; import { ChatTipService, IChatTipService } from './chatTipService.js'; import { ChatQueuePickerRendering } from './widget/input/chatQueuePickerActionItem.js'; +import { ExploreAgentDefaultModel } from './exploreAgentDefaultModel.js'; import { PlanAgentDefaultModel } from './planAgentDefaultModel.js'; const toolReferenceNameEnumValues: string[] = []; @@ -626,6 +627,14 @@ configurationRegistry.registerConfiguration({ enumItemLabels: PlanAgentDefaultModel.modelLabels, markdownEnumDescriptions: PlanAgentDefaultModel.modelDescriptions }, + [ChatConfiguration.ExploreAgentDefaultModel]: { + type: 'string', + description: nls.localize('chat.exploreAgent.defaultModel.description', "Select the default language model to use for the Explore subagent from the available providers."), + default: '', + enum: ExploreAgentDefaultModel.modelIds, + enumItemLabels: ExploreAgentDefaultModel.modelLabels, + markdownEnumDescriptions: ExploreAgentDefaultModel.modelDescriptions + }, [ChatConfiguration.RequestQueueingEnabled]: { type: 'boolean', description: nls.localize('chat.requestQueuing.enabled.description', "When enabled, allows queuing additional messages while a request is in progress and steering the current request with a new message."), diff --git a/src/vs/workbench/contrib/chat/browser/defaultModelContribution.ts b/src/vs/workbench/contrib/chat/browser/defaultModelContribution.ts new file mode 100644 index 00000000000..8af7a3622b9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/defaultModelContribution.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/languageModels.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../common/widget/input/modelPickerWidget.js'; + +const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); + +export interface DefaultModelArrays { + readonly modelIds: string[]; + readonly modelLabels: string[]; + readonly modelDescriptions: string[]; +} + +export interface DefaultModelContributionOptions { + /** Configuration key for the setting (used in schema notification). */ + readonly configKey: string; + /** Configuration section id for `notifyConfigurationSchemaUpdated`, or `undefined` to skip notification. */ + readonly configSectionId: string | undefined; + /** Log prefix, e.g. `'[PlanAgentDefaultModel]'`. */ + readonly logPrefix: string; + /** Additional filter beyond `isUserSelectable`. Return `true` to include the model. */ + readonly filter?: (metadata: ILanguageModelChatMetadata) => boolean; +} + +/** + * Creates the initial static arrays used by configuration registration code. + * The returned arrays are mutated in-place by {@link DefaultModelContribution}. + */ +export function createDefaultModelArrays(): DefaultModelArrays { + return { + modelIds: [''], + modelLabels: [localize('defaultModel', 'Auto (Vendor Default)')], + modelDescriptions: [localize('defaultModelDescription', "Use the vendor's default model")], + }; +} + +/** + * Shared base class for workbench contributions that populate a dynamic enum + * of language models for a settings picker. + */ +export abstract class DefaultModelContribution extends Disposable { + + constructor( + private readonly _arrays: DefaultModelArrays, + private readonly _options: DefaultModelContributionOptions, + @ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + this._register(_languageModelsService.onDidChangeLanguageModels(() => this._updateModelValues())); + this._updateModelValues(); + } + + private _updateModelValues(): void { + const { modelIds, modelLabels, modelDescriptions } = this._arrays; + const { configKey, configSectionId, logPrefix, filter } = this._options; + + try { + // Clear arrays + modelIds.length = 0; + modelLabels.length = 0; + modelDescriptions.length = 0; + + // Add default/empty option + modelIds.push(''); + modelLabels.push(localize('defaultModel', 'Auto (Vendor Default)')); + modelDescriptions.push(localize('defaultModelDescription', "Use the vendor's default model")); + + const models: { identifier: string; metadata: ILanguageModelChatMetadata }[] = []; + const allModelIds = this._languageModelsService.getLanguageModelIds(); + + for (const modelId of allModelIds) { + try { + const metadata = this._languageModelsService.lookupLanguageModel(modelId); + if (metadata) { + models.push({ identifier: modelId, metadata }); + } else { + this._logService.warn(`${logPrefix} No metadata found for model ID: ${modelId}`); + } + } catch (e) { + this._logService.error(`${logPrefix} Error looking up model ${modelId}:`, e); + } + } + + const supportedModels = models.filter(model => { + if (!model.metadata?.isUserSelectable) { + return false; + } + if (filter && !filter(model.metadata)) { + return false; + } + return true; + }); + + supportedModels.sort((a, b) => { + const aCategory = a.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; + const bCategory = b.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; + + if (aCategory.order !== bCategory.order) { + return aCategory.order - bCategory.order; + } + + return a.metadata.name.localeCompare(b.metadata.name); + }); + + for (const model of supportedModels) { + try { + const qualifiedName = ILanguageModelChatMetadata.asQualifiedName(model.metadata); + modelIds.push(qualifiedName); + modelLabels.push(model.metadata.name); + modelDescriptions.push(model.metadata.tooltip ?? model.metadata.detail ?? ''); + } catch (e) { + this._logService.error(`${logPrefix} Error adding model ${model.metadata.name}:`, e); + } + } + + if (configSectionId) { + configurationRegistry.notifyConfigurationSchemaUpdated({ + id: configSectionId, + properties: { + [configKey]: {} + } + }); + } + } catch (e) { + this._logService.error(`${logPrefix} Error updating model values:`, e); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/exploreAgentDefaultModel.ts b/src/vs/workbench/contrib/chat/browser/exploreAgentDefaultModel.ts new file mode 100644 index 00000000000..b20d98e4215 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/exploreAgentDefaultModel.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService } from '../../../../platform/log/common/log.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { ChatConfiguration } from '../common/constants.js'; +import { ILanguageModelsService } from '../common/languageModels.js'; +import { createDefaultModelArrays, DefaultModelContribution } from './defaultModelContribution.js'; + +const arrays = createDefaultModelArrays(); + +export class ExploreAgentDefaultModel extends DefaultModelContribution { + static readonly ID = 'workbench.contrib.exploreAgentDefaultModel'; + + static readonly modelIds = arrays.modelIds; + static readonly modelLabels = arrays.modelLabels; + static readonly modelDescriptions = arrays.modelDescriptions; + + constructor( + @ILanguageModelsService languageModelsService: ILanguageModelsService, + @ILogService logService: ILogService, + ) { + super(arrays, { + configKey: ChatConfiguration.ExploreAgentDefaultModel, + configSectionId: 'chatSidebar', + logPrefix: '[ExploreAgentDefaultModel]', + filter: metadata => !!metadata.capabilities?.toolCalling, + }, languageModelsService, logService); + } +} + +registerWorkbenchContribution2(ExploreAgentDefaultModel.ID, ExploreAgentDefaultModel, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/chat/browser/planAgentDefaultModel.ts b/src/vs/workbench/contrib/chat/browser/planAgentDefaultModel.ts index 37c49f0774f..e6a8cd31943 100644 --- a/src/vs/workbench/contrib/chat/browser/planAgentDefaultModel.ts +++ b/src/vs/workbench/contrib/chat/browser/planAgentDefaultModel.ts @@ -3,104 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ChatConfiguration } from '../common/constants.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../common/languageModels.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY } from '../common/widget/input/modelPickerWidget.js'; +import { ILanguageModelsService } from '../common/languageModels.js'; +import { createDefaultModelArrays, DefaultModelContribution } from './defaultModelContribution.js'; -const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); +const arrays = createDefaultModelArrays(); -export class PlanAgentDefaultModel extends Disposable { +export class PlanAgentDefaultModel extends DefaultModelContribution { static readonly ID = 'workbench.contrib.planAgentDefaultModel'; - static readonly configName = ChatConfiguration.PlanAgentDefaultModel; - static modelIds: string[] = ['']; - static modelLabels: string[] = [localize('defaultModel', 'Auto (Vendor Default)')]; - static modelDescriptions: string[] = [localize('defaultModelDescription', "Use the vendor's default model")]; + static readonly modelIds = arrays.modelIds; + static readonly modelLabels = arrays.modelLabels; + static readonly modelDescriptions = arrays.modelDescriptions; constructor( - @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @ILogService private readonly logService: ILogService, + @ILanguageModelsService languageModelsService: ILanguageModelsService, + @ILogService logService: ILogService, ) { - super(); - this._register(languageModelsService.onDidChangeLanguageModels(() => this._updateModelValues())); - this._updateModelValues(); - } - - private _updateModelValues(): void { - try { - // Clear arrays - PlanAgentDefaultModel.modelIds.length = 0; - PlanAgentDefaultModel.modelLabels.length = 0; - PlanAgentDefaultModel.modelDescriptions.length = 0; - - // Add default/empty option - PlanAgentDefaultModel.modelIds.push(''); - PlanAgentDefaultModel.modelLabels.push(localize('defaultModel', 'Auto (Vendor Default)')); - PlanAgentDefaultModel.modelDescriptions.push(localize('defaultModelDescription', "Use the vendor's default model")); - - const models: { identifier: string; metadata: ILanguageModelChatMetadata }[] = []; - const modelIds = this.languageModelsService.getLanguageModelIds(); - - for (const modelId of modelIds) { - try { - const metadata = this.languageModelsService.lookupLanguageModel(modelId); - if (metadata) { - models.push({ identifier: modelId, metadata }); - } else { - this.logService.warn(`[PlanAgentDefaultModel] No metadata found for model ID: ${modelId}`); - } - } catch (e) { - this.logService.error(`[PlanAgentDefaultModel] Error looking up model ${modelId}:`, e); - } - } - - const supportedModels = models.filter(model => { - if (!model.metadata?.isUserSelectable) { - return false; - } - if (!model.metadata.capabilities?.toolCalling) { - return false; - } - return true; - }); - - supportedModels.sort((a, b) => { - const aCategory = a.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; - const bCategory = b.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; - - if (aCategory.order !== bCategory.order) { - return aCategory.order - bCategory.order; - } - - return a.metadata.name.localeCompare(b.metadata.name); - }); - - for (const model of supportedModels) { - try { - const qualifiedName = `${model.metadata.name} (${model.metadata.vendor})`; - PlanAgentDefaultModel.modelIds.push(qualifiedName); - PlanAgentDefaultModel.modelLabels.push(model.metadata.name); - PlanAgentDefaultModel.modelDescriptions.push(model.metadata.tooltip ?? model.metadata.detail ?? ''); - } catch (e) { - this.logService.error(`[PlanAgentDefaultModel] Error adding model ${model.metadata.name}:`, e); - } - } - - configurationRegistry.notifyConfigurationSchemaUpdated({ - id: 'chatSidebar', - properties: { - [ChatConfiguration.PlanAgentDefaultModel]: {} - } - }); - } catch (e) { - this.logService.error('[PlanAgentDefaultModel] Error updating model values:', e); - } + super(arrays, { + configKey: ChatConfiguration.PlanAgentDefaultModel, + configSectionId: 'chatSidebar', + logPrefix: '[PlanAgentDefaultModel]', + filter: metadata => !!metadata.capabilities?.toolCalling, + }, languageModelsService, logService); } } diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index 2ac63ee13ea..2440311f09b 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -12,6 +12,7 @@ export enum ChatConfiguration { AIDisabled = 'chat.disableAIFeatures', AgentEnabled = 'chat.agent.enabled', PlanAgentDefaultModel = 'chat.planAgent.defaultModel', + ExploreAgentDefaultModel = 'chat.exploreAgent.defaultModel', RequestQueueingEnabled = 'chat.requestQueuing.enabled', RequestQueueingDefaultAction = 'chat.requestQueuing.defaultAction', AgentStatusEnabled = 'chat.agentsControl.enabled', diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts index 20286d86cd7..5fd53270237 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts @@ -5,104 +5,32 @@ import { localize } from '../../../../nls.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { ILanguageModelsService, ILanguageModelChatMetadata } from '../../chat/common/languageModels.js'; +import { ILanguageModelsService } from '../../chat/common/languageModels.js'; import { InlineChatConfigKeys } from '../common/inlineChat.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../chat/common/widget/input/modelPickerWidget.js'; +import { createDefaultModelArrays, DefaultModelContribution } from '../../chat/browser/defaultModelContribution.js'; -export class InlineChatDefaultModel extends Disposable { +const arrays = createDefaultModelArrays(); + +export class InlineChatDefaultModel extends DefaultModelContribution { static readonly ID = 'workbench.contrib.inlineChatDefaultModel'; - static readonly configName = InlineChatConfigKeys.DefaultModel; - static modelIds: string[] = ['']; - static modelLabels: string[] = [localize('defaultModel', 'Auto (Vendor Default)')]; - static modelDescriptions: string[] = [localize('defaultModelDescription', 'Use the vendor\'s default model')]; + static readonly modelIds = arrays.modelIds; + static readonly modelLabels = arrays.modelLabels; + static readonly modelDescriptions = arrays.modelDescriptions; constructor( - @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @ILogService private readonly logService: ILogService, + @ILanguageModelsService languageModelsService: ILanguageModelsService, + @ILogService logService: ILogService, ) { - super(); - this._register(languageModelsService.onDidChangeLanguageModels(() => this._updateModelValues())); - this._updateModelValues(); - } - - private _updateModelValues(): void { - try { - // Clear arrays - InlineChatDefaultModel.modelIds.length = 0; - InlineChatDefaultModel.modelLabels.length = 0; - InlineChatDefaultModel.modelDescriptions.length = 0; - - // Add default/empty option - InlineChatDefaultModel.modelIds.push(''); - InlineChatDefaultModel.modelLabels.push(localize('defaultModel', 'Auto (Vendor Default)')); - InlineChatDefaultModel.modelDescriptions.push(localize('defaultModelDescription', 'Use the vendor\'s default model')); - - // Get all available models - const modelIds = this.languageModelsService.getLanguageModelIds(); - - const models: { identifier: string; metadata: ILanguageModelChatMetadata }[] = []; - - // Look up each model's metadata - for (const modelId of modelIds) { - try { - const metadata = this.languageModelsService.lookupLanguageModel(modelId); - if (metadata) { - models.push({ identifier: modelId, metadata }); - } else { - this.logService.warn(`[InlineChatDefaultModel] No metadata found for model ID: ${modelId}`); - } - } catch (e) { - this.logService.error(`[InlineChatDefaultModel] Error looking up model ${modelId}:`, e); - } - } - - // Filter models that are: - // 1. User selectable - // 2. Support tool calling (required for inline chat v2) - const supportedModels = models.filter(model => { - if (!model.metadata?.isUserSelectable) { - return false; - } - // Check if model supports inline chat - needs tool calling capability - if (!model.metadata.capabilities?.toolCalling) { - return false; - } - return true; - }); - - // Sort by category order, then alphabetically by name within each category - supportedModels.sort((a, b) => { - const aCategory = a.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; - const bCategory = b.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; - - // First sort by category order - if (aCategory.order !== bCategory.order) { - return aCategory.order - bCategory.order; - } - - // Then sort by name within the same category - return a.metadata.name.localeCompare(b.metadata.name); - }); - - // Populate arrays with filtered models - for (const model of supportedModels) { - try { - const qualifiedName = `${model.metadata.name} (${model.metadata.vendor})`; - InlineChatDefaultModel.modelIds.push(qualifiedName); - InlineChatDefaultModel.modelLabels.push(model.metadata.name); - InlineChatDefaultModel.modelDescriptions.push(model.metadata.tooltip ?? model.metadata.detail ?? ''); - } catch (e) { - this.logService.error(`[InlineChatDefaultModel] Error adding model ${model.metadata.name}:`, e); - } - } - } catch (e) { - this.logService.error('[InlineChatDefaultModel] Error updating model values:', e); - } + super(arrays, { + configKey: InlineChatConfigKeys.DefaultModel, + configSectionId: 'inlineChat', + logPrefix: '[InlineChatDefaultModel]', + filter: metadata => !!metadata.capabilities?.toolCalling, + }, languageModelsService, logService); } } @@ -111,7 +39,7 @@ registerWorkbenchContribution2(InlineChatDefaultModel.ID, InlineChatDefaultModel Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ ...{ id: 'inlineChat', title: localize('inlineChatConfigurationTitle', 'Inline Chat'), order: 30, type: 'object' }, properties: { - [InlineChatDefaultModel.configName]: { + [InlineChatConfigKeys.DefaultModel]: { description: localize('inlineChatDefaultModelDescription', "Select the default language model to use for inline chat from the available providers. Model names may include the provider in parentheses, for example 'Claude Haiku 4.5 (copilot)'."), type: 'string', default: '',