Path: blob/main/src/vs/sessions/contrib/chat/browser/agentHost/agentHostModelPicker.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { BaseActionViewItem } from '../../../../../base/browser/ui/actionbar/actionViewItems.js';6import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';7import { autorun, observableValue } from '../../../../../base/common/observable.js';8import * as nls from '../../../../../nls.js';9import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';10import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';11import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';12import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';13import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';14import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../workbench/common/contributions.js';15import { type ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';16import { type IChatInputPickerOptions } from '../../../../../workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.js';17import { ModelPickerActionItem, type IModelPickerDelegate } from '../../../../../workbench/contrib/chat/browser/widget/input/modelPickerActionItem.js';18import { ActiveSessionProviderIdContext } from '../../../../common/contextkeys.js';19import { type ISession } from '../../../../services/sessions/common/session.js';20import { ISessionsManagementService } from '../../../../services/sessions/common/sessionsManagement.js';21import { ISessionsProvidersService } from '../../../../services/sessions/browser/sessionsProvidersService.js';22import { Menus } from '../../../../browser/menus.js';23import { LOCAL_AGENT_HOST_PROVIDER_ID, REMOTE_AGENT_HOST_PROVIDER_RE } from '../../../../common/agentHostSessionsProvider.js';2425const IsActiveSessionAgentHost = ContextKeyExpr.or(26ContextKeyExpr.equals(ActiveSessionProviderIdContext.key, LOCAL_AGENT_HOST_PROVIDER_ID),27ContextKeyExpr.regex(ActiveSessionProviderIdContext.key, REMOTE_AGENT_HOST_PROVIDER_RE),28);2930// -- Agent Host Model Picker Action --3132registerAction2(class extends Action2 {33constructor() {34super({35id: 'sessions.agentHost.modelPicker',36title: nls.localize2('agentHostModelPicker', "Model"),37f1: false,38menu: [{39id: Menus.NewSessionConfig,40group: 'navigation',41order: 1,42when: IsActiveSessionAgentHost,43}],44});45}46override async run(): Promise<void> { /* handled by action view item */ }47});4849// -- Agent Host Model Picker Contribution --5051function getAgentHostModels(52languageModelsService: ILanguageModelsService,53session: ISession | undefined,54): ILanguageModelChatMetadataAndIdentifier[] {55if (!session) {56return [];57}58// Filter models by resource scheme. For remote agent hosts the scheme is59// a unique per-connection ID; for local agent hosts it equals the session60// type. Both are used as the targetChatSessionType when registering61// models via AgentHostLanguageModelProvider.62const resourceScheme = session.resource.scheme;63return languageModelsService.getLanguageModelIds()64.map(id => {65const metadata = languageModelsService.lookupLanguageModel(id);66return metadata ? { metadata, identifier: id } : undefined;67})68.filter((m): m is ILanguageModelChatMetadataAndIdentifier => !!m && m.metadata.targetChatSessionType === resourceScheme);69}7071const STORAGE_KEY = 'sessions.agentHostModelPicker.selectedModelId';7273class AgentHostModelPickerContribution extends Disposable implements IWorkbenchContribution {7475static readonly ID = 'sessions.contrib.agentHostModelPicker';7677constructor(78@IActionViewItemService actionViewItemService: IActionViewItemService,79@IInstantiationService instantiationService: IInstantiationService,80@ILanguageModelsService languageModelsService: ILanguageModelsService,81@ISessionsManagementService sessionsManagementService: ISessionsManagementService,82@ISessionsProvidersService sessionsProvidersService: ISessionsProvidersService,83@IStorageService storageService: IStorageService,84) {85super();8687this._register(actionViewItemService.register(88Menus.NewSessionConfig, 'sessions.agentHost.modelPicker',89() => {90const currentModel = observableValue<ILanguageModelChatMetadataAndIdentifier | undefined>('currentModel', undefined);91const delegate: IModelPickerDelegate = {92currentModel,93setModel: (model: ILanguageModelChatMetadataAndIdentifier) => {94currentModel.set(model, undefined);95storageService.store(STORAGE_KEY, model.identifier, StorageScope.PROFILE, StorageTarget.MACHINE);96const session = sessionsManagementService.activeSession.get();97if (session) {98const provider = sessionsProvidersService.getProviders().find(p => p.id === session.providerId);99provider?.setModel(session.sessionId, model.identifier);100}101},102getModels: () => getAgentHostModels(languageModelsService, sessionsManagementService.activeSession.get()),103useGroupedModelPicker: () => true,104showManageModelsAction: () => false,105showUnavailableFeatured: () => false,106showFeatured: () => true,107};108const pickerOptions: IChatInputPickerOptions = {109hideChevrons: observableValue('hideChevrons', false),110};111const action = { id: 'sessions.agentHost.modelPicker', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } };112const modelPicker = instantiationService.createInstance(ModelPickerActionItem, action, delegate, pickerOptions);113114const rememberedModelId = storageService.get(STORAGE_KEY, StorageScope.PROFILE);115const initModel = (session: ISession | undefined, sessionModelId: string | undefined) => {116const models = getAgentHostModels(languageModelsService, session);117modelPicker.setEnabled(models.length > 0);118119let resolvedModel = sessionModelId120? models.find(model => model.identifier === sessionModelId)121: undefined;122123// When no model is explicitly selected, restore the124// remembered model or pick the first available one so125// the picker shows a real model name instead of the126// misleading "Auto" label (the copilot "auto"127// pseudo-model is not available in agent host sessions).128if (!resolvedModel && models.length > 0) {129const remembered = rememberedModelId ? models.find(m => m.identifier === rememberedModelId) : undefined;130resolvedModel = remembered ?? models[0];131delegate.setModel(resolvedModel);132}133134currentModel.set(resolvedModel, undefined);135};136const initModelFromActiveSession = () => {137const session = sessionsManagementService.activeSession.get();138initModel(session, session?.modelId.get());139};140initModelFromActiveSession();141142const disposableStore = new DisposableStore();143disposableStore.add(languageModelsService.onDidChangeLanguageModels(() => initModelFromActiveSession()));144145disposableStore.add(autorun(reader => {146const session = sessionsManagementService.activeSession.read(reader);147const sessionModelId = session?.modelId.read(reader);148initModel(session, sessionModelId);149}));150151// When the active session changes, push the selected model to the new session152disposableStore.add(autorun(reader => {153const session = sessionsManagementService.activeSession.read(reader);154const model = currentModel.read(reader);155if (session && model) {156const provider = sessionsProvidersService.getProviders().find(p => p.id === session.providerId);157provider?.setModel(session.sessionId, model.identifier);158}159}));160161return new AgentHostPickerActionViewItem(modelPicker, disposableStore);162},163));164}165}166167class AgentHostPickerActionViewItem extends BaseActionViewItem {168constructor(private readonly picker: { render(container: HTMLElement): void; dispose(): void }, disposable?: DisposableStore) {169super(undefined, { id: '', label: '', enabled: true, class: undefined, tooltip: '', run: () => { } });170if (disposable) {171this._register(disposable);172}173}174175override render(container: HTMLElement): void {176this.picker.render(container);177}178179override dispose(): void {180this.picker.dispose();181super.dispose();182}183}184185registerWorkbenchContribution2(AgentHostModelPickerContribution.ID, AgentHostModelPickerContribution, WorkbenchPhase.AfterRestored);186187188