Merge pull request #294779 from microsoft/digitarald/explore-agent-default-model

Add Explore agent default model picker and refactor DefaultModelContribution
This commit is contained in:
Harald Kirschner
2026-02-12 16:03:46 -08:00
committed by GitHub
parent aad1561e22
commit 73a9d49dea
6 changed files with 214 additions and 178 deletions

View File

@@ -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."),

View File

@@ -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<IConfigurationRegistry>(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);
}
}
}

View File

@@ -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);

View File

@@ -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<IConfigurationRegistry>(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);
}
}

View File

@@ -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',

View File

@@ -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<IConfigurationRegistry>(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: '',