mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-28 06:31:58 +00:00
show inline install X recommendations for alternative agents (#280738)
* show inline install X recommendations for alternative agents
* polish
* move to ChatAgentRecommendation contribution
* fix incorrect registrations
* no f1
* update distro f7daaf6841
This commit is contained in:
parent
b675b458ed
commit
34e8399e06
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"version": "1.107.0",
|
||||
"distro": "55e124105c9ac2939053cdc9f50ff7d84fc86294",
|
||||
"distro": "f7daaf68414ef6e47bec698f8babc297f0d90f0d",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
},
|
||||
|
||||
@ -43,6 +43,7 @@ export interface IChatSessionRecommendation {
|
||||
readonly displayName: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly postInstallCommand?: string;
|
||||
}
|
||||
|
||||
export type ConfigurationSyncStore = {
|
||||
|
||||
@ -226,6 +226,7 @@ export class MenuId {
|
||||
static readonly TimelineFilterSubMenu = new MenuId('TimelineFilterSubMenu');
|
||||
static readonly AgentSessionsTitle = new MenuId('AgentSessionsTitle');
|
||||
static readonly AgentSessionsFilterSubMenu = new MenuId('AgentSessionsFilterSubMenu');
|
||||
static readonly AgentSessionsInstallActions = new MenuId('AgentSessionsInstallActions');
|
||||
static readonly AgentSessionItemToolbar = new MenuId('AgentSessionItemToolbar');
|
||||
static readonly AccountsContext = new MenuId('AccountsContext');
|
||||
static readonly SidebarTitle = new MenuId('SidebarTitle');
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { TimeoutTimer } from '../../../../../base/common/async.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { localize2 } from '../../../../../nls.js';
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IExtensionGalleryService } from '../../../../../platform/extensionManagement/common/extensionManagement.js';
|
||||
import { ICommandService, CommandsRegistry } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { IWorkbenchContribution } from '../../../../common/contributions.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IWorkbenchExtensionManagementService } from '../../../../services/extensionManagement/common/extensionManagement.js';
|
||||
import { CHAT_CATEGORY } from './chatActions.js';
|
||||
import { IChatSessionRecommendation } from '../../../../../base/common/product.js';
|
||||
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
|
||||
|
||||
const INSTALL_CONTEXT_PREFIX = 'chat.installRecommendationAvailable';
|
||||
|
||||
export class ChatAgentRecommendation extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.chatAgentRecommendation';
|
||||
|
||||
private readonly availabilityContextKeys = new Map<string, IContextKey<boolean>>();
|
||||
private refreshRequestId = 0;
|
||||
|
||||
constructor(
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
|
||||
@IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const recommendations = this.productService.chatSessionRecommendations;
|
||||
if (!recommendations?.length || !this.extensionGalleryService.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const recommendation of recommendations) {
|
||||
this.registerRecommendation(recommendation);
|
||||
}
|
||||
|
||||
this.refreshInstallAvailability();
|
||||
|
||||
const refresh = () => this.refreshInstallAvailability();
|
||||
this._register(this.extensionManagementService.onProfileAwareDidInstallExtensions(refresh));
|
||||
this._register(this.extensionManagementService.onProfileAwareDidUninstallExtension(refresh));
|
||||
this._register(this.extensionManagementService.onDidChangeProfile(refresh));
|
||||
}
|
||||
|
||||
private registerRecommendation(recommendation: IChatSessionRecommendation): void {
|
||||
const extensionKey = ExtensionIdentifier.toKey(recommendation.extensionId);
|
||||
const commandId = `chat.installRecommendation.${extensionKey}`;
|
||||
const availabilityContextId = `${INSTALL_CONTEXT_PREFIX}.${extensionKey}`;
|
||||
const availabilityContext = new RawContextKey<boolean>(availabilityContextId, false).bindTo(this.contextKeyService);
|
||||
this.availabilityContextKeys.set(extensionKey, availabilityContext);
|
||||
|
||||
const title = localize2('chat.installRecommendation', "Install {0}", recommendation.displayName);
|
||||
|
||||
this._register(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: commandId,
|
||||
title,
|
||||
tooltip: recommendation.description,
|
||||
f1: false,
|
||||
category: CHAT_CATEGORY,
|
||||
icon: Codicon.extensions,
|
||||
precondition: ContextKeyExpr.equals(availabilityContextId, true),
|
||||
menu: [
|
||||
{
|
||||
id: MenuId.AgentSessionsInstallActions,
|
||||
group: '0_install',
|
||||
when: ContextKeyExpr.equals(availabilityContextId, true)
|
||||
},
|
||||
{
|
||||
id: MenuId.AgentSessionsTitle,
|
||||
group: 'navigation@98',
|
||||
when: ContextKeyExpr.equals(availabilityContextId, true)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const productService = accessor.get(IProductService);
|
||||
|
||||
const installPreReleaseVersion = productService.quality !== 'stable';
|
||||
await commandService.executeCommand('workbench.extensions.installExtension', recommendation.extensionId, {
|
||||
installPreReleaseVersion
|
||||
});
|
||||
|
||||
await runPostInstallCommand(commandService, recommendation.postInstallCommand);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private refreshInstallAvailability(): void {
|
||||
if (!this.availabilityContextKeys.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentRequest = ++this.refreshRequestId;
|
||||
this.extensionManagementService.getInstalled().then(installedExtensions => {
|
||||
if (currentRequest !== this.refreshRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installed = new Set(installedExtensions.map(ext => ExtensionIdentifier.toKey(ext.identifier.id)));
|
||||
for (const [extensionKey, context] of this.availabilityContextKeys) {
|
||||
context.set(!installed.has(extensionKey));
|
||||
}
|
||||
}, () => {
|
||||
if (currentRequest !== this.refreshRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [, context] of this.availabilityContextKeys) {
|
||||
context.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function runPostInstallCommand(commandService: ICommandService, commandId: string | undefined): Promise<void> {
|
||||
if (!commandId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await waitForCommandRegistration(commandId);
|
||||
|
||||
try {
|
||||
await commandService.executeCommand(commandId);
|
||||
} catch {
|
||||
// Command failed or was cancelled; ignore.
|
||||
}
|
||||
}
|
||||
|
||||
function waitForCommandRegistration(commandId: string): Promise<void> {
|
||||
if (CommandsRegistry.getCommands().has(commandId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise<void>(resolve => {
|
||||
const timer = new TimeoutTimer();
|
||||
const listener = CommandsRegistry.onDidRegisterCommand((id: string) => {
|
||||
if (id === commandId) {
|
||||
listener.dispose();
|
||||
timer.dispose();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
timer.cancelAndSet(() => {
|
||||
listener.dispose();
|
||||
resolve();
|
||||
}, 10_000);
|
||||
});
|
||||
}
|
||||
@ -3,11 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { KeyCode } from '../../../../../base/common/keyCodes.js';
|
||||
import { MarshalledId } from '../../../../../base/common/marshallingIds.js';
|
||||
import { IChatSessionRecommendation } from '../../../../../base/common/product.js';
|
||||
import Severity from '../../../../../base/common/severity.js';
|
||||
import * as nls from '../../../../../nls.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
@ -15,14 +13,10 @@ import { Action2, MenuId, MenuRegistry } from '../../../../../platform/actions/c
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { IExtensionGalleryService } from '../../../../../platform/extensionManagement/common/extensionManagement.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ILogService } from '../../../../../platform/log/common/log.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../../services/editor/common/editorService.js';
|
||||
import { IWorkbenchExtensionManagementService } from '../../../../services/extensionManagement/common/extensionManagement.js';
|
||||
import { IViewsService } from '../../../../services/views/common/viewsService.js';
|
||||
import { ChatContextKeys } from '../../common/chatContextKeys.js';
|
||||
import { IChatService } from '../../common/chatService.js';
|
||||
@ -313,65 +307,6 @@ export class ToggleAgentSessionsViewLocationAction extends Action2 {
|
||||
}
|
||||
}
|
||||
|
||||
export class ChatSessionsGettingStartedAction extends Action2 {
|
||||
static readonly ID = 'chat.sessions.gettingStarted';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ChatSessionsGettingStartedAction.ID,
|
||||
title: nls.localize2('chat.sessions.gettingStarted.action', "Getting Started with Chat Sessions"),
|
||||
icon: Codicon.sendToRemoteAgent,
|
||||
f1: false,
|
||||
});
|
||||
}
|
||||
|
||||
override async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const productService = accessor.get(IProductService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const extensionManagementService = accessor.get(IWorkbenchExtensionManagementService);
|
||||
const extensionGalleryService = accessor.get(IExtensionGalleryService);
|
||||
|
||||
const recommendations = productService.chatSessionRecommendations;
|
||||
if (!recommendations || recommendations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installedExtensions = await extensionManagementService.getInstalled();
|
||||
const isExtensionAlreadyInstalled = (extensionId: string) => {
|
||||
return installedExtensions.find(installed => installed.identifier.id === extensionId);
|
||||
};
|
||||
|
||||
const quickPickItems = recommendations.map((recommendation: IChatSessionRecommendation) => {
|
||||
const extensionInstalled = !!isExtensionAlreadyInstalled(recommendation.extensionId);
|
||||
return {
|
||||
label: recommendation.displayName,
|
||||
description: recommendation.description,
|
||||
detail: extensionInstalled
|
||||
? nls.localize('chatSessions.extensionAlreadyInstalled', "'{0}' is already installed", recommendation.extensionName)
|
||||
: nls.localize('chatSessions.installExtension', "Installs '{0}'", recommendation.extensionName),
|
||||
extensionId: recommendation.extensionId,
|
||||
disabled: extensionInstalled,
|
||||
};
|
||||
});
|
||||
|
||||
const selected = await quickInputService.pick(quickPickItems, {
|
||||
title: nls.localize('chatSessions.selectExtension', "Install Agents..."),
|
||||
placeHolder: nls.localize('chatSessions.pickPlaceholder', "Install agents from the extension marketplace"),
|
||||
canPickMany: true,
|
||||
});
|
||||
|
||||
if (!selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const galleryExtensions = await extensionGalleryService.getExtensions(selected.map(item => ({ id: item.extensionId })), CancellationToken.None);
|
||||
if (!galleryExtensions) {
|
||||
return;
|
||||
}
|
||||
await extensionManagementService.installGalleryExtensions(galleryExtensions.map(extension => ({ extension, options: { preRelease: productService.quality !== 'stable' } })));
|
||||
}
|
||||
}
|
||||
|
||||
// Register the menu item - show for all local chat sessions (including history items)
|
||||
MenuRegistry.appendMenuItem(MenuId.ChatSessionsMenu, {
|
||||
command: {
|
||||
|
||||
@ -88,6 +88,7 @@ export class AgentSessionsView extends ViewPane {
|
||||
() => didResolve.p
|
||||
);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
protected override renderBody(container: HTMLElement): void {
|
||||
@ -185,13 +186,12 @@ export class AgentSessionsView extends ViewPane {
|
||||
}
|
||||
}
|
||||
|
||||
// Install more
|
||||
actions.push(new Separator());
|
||||
actions.push(toAction({
|
||||
id: 'install-extensions',
|
||||
label: localize('chatSessions.installExtensions', "Install Agents..."),
|
||||
run: () => this.commandService.executeCommand('chat.sessions.gettingStarted')
|
||||
}));
|
||||
const installMenuActions = this.menuService.getMenuActions(MenuId.AgentSessionsInstallActions, this.scopedContextKeyService, { shouldForwardArgs: true });
|
||||
const installActionBar = getActionBarActions(installMenuActions, () => true);
|
||||
if (installActionBar.primary.length > 0) {
|
||||
actions.push(new Separator());
|
||||
actions.push(...installActionBar.primary);
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
@ -80,7 +80,8 @@ import { registerMoveActions } from './actions/chatMoveActions.js';
|
||||
import { registerNewChatActions } from './actions/chatNewActions.js';
|
||||
import { registerChatPromptNavigationActions } from './actions/chatPromptNavigationActions.js';
|
||||
import { registerQuickChatActions } from './actions/chatQuickInputActions.js';
|
||||
import { ChatSessionsGettingStartedAction, DeleteChatSessionAction, OpenChatSessionInNewEditorGroupAction, OpenChatSessionInNewWindowAction, OpenChatSessionInSidebarAction, RenameChatSessionAction, ToggleAgentSessionsViewLocationAction, ToggleChatSessionsDescriptionDisplayAction } from './actions/chatSessionActions.js';
|
||||
import { ChatAgentRecommendation } from './actions/chatAgentRecommendationActions.js';
|
||||
import { DeleteChatSessionAction, OpenChatSessionInNewEditorGroupAction, OpenChatSessionInNewWindowAction, OpenChatSessionInSidebarAction, RenameChatSessionAction, ToggleAgentSessionsViewLocationAction, ToggleChatSessionsDescriptionDisplayAction } from './actions/chatSessionActions.js';
|
||||
import { registerChatTitleActions } from './actions/chatTitleActions.js';
|
||||
import { registerChatElicitationActions } from './actions/chatElicitationActions.js';
|
||||
import { registerChatToolActions } from './actions/chatToolActions.js';
|
||||
@ -1173,6 +1174,7 @@ registerWorkbenchContribution2(BuiltinToolsContribution.ID, BuiltinToolsContribu
|
||||
registerWorkbenchContribution2(ChatAgentSettingContribution.ID, ChatAgentSettingContribution, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatAgentActionsContribution.ID, ChatAgentActionsContribution, WorkbenchPhase.Eventually);
|
||||
registerWorkbenchContribution2(ToolReferenceNamesContribution.ID, ToolReferenceNamesContribution, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatAgentRecommendation.ID, ChatAgentRecommendation, WorkbenchPhase.Eventually);
|
||||
registerWorkbenchContribution2(ChatEditingEditorAccessibility.ID, ChatEditingEditorAccessibility, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatEditingEditorOverlay.ID, ChatEditingEditorOverlay, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(SimpleBrowserOverlay.ID, SimpleBrowserOverlay, WorkbenchPhase.AfterRestored);
|
||||
@ -1247,7 +1249,6 @@ registerAction2(OpenChatSessionInNewWindowAction);
|
||||
registerAction2(OpenChatSessionInNewEditorGroupAction);
|
||||
registerAction2(OpenChatSessionInSidebarAction);
|
||||
registerAction2(ToggleChatSessionsDescriptionDisplayAction);
|
||||
registerAction2(ChatSessionsGettingStartedAction);
|
||||
registerAction2(ToggleAgentSessionsViewLocationAction);
|
||||
|
||||
ChatWidget.CONTRIBS.push(ChatDynamicVariableModel);
|
||||
|
||||
@ -8,7 +8,7 @@ import { raceCancellationError } from '../../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { MutableDisposable, combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ResourceMap } from '../../../../base/common/map.js';
|
||||
import { Schemas } from '../../../../base/common/network.js';
|
||||
import * as resources from '../../../../base/common/resources.js';
|
||||
@ -633,8 +633,31 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
|
||||
const disposableStore = new DisposableStore();
|
||||
this._contributionDisposables.set(contribution.type, disposableStore);
|
||||
|
||||
disposableStore.add(this._registerAgent(contribution, ext));
|
||||
disposableStore.add(this._registerCommands(contribution));
|
||||
const providerDependentDisposables = new MutableDisposable<IDisposable>();
|
||||
disposableStore.add(providerDependentDisposables);
|
||||
|
||||
// NOTE: Only those extensions that register as 'content providers' should have agents and commands auto-registered
|
||||
// This relationship may change in the future as the API grows.
|
||||
const reconcileProviderRegistrations = () => {
|
||||
if (this._contentProviders.has(contribution.type)) {
|
||||
if (!providerDependentDisposables.value) {
|
||||
providerDependentDisposables.value = combinedDisposable(
|
||||
this._registerAgent(contribution, ext),
|
||||
this._registerCommands(contribution)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
providerDependentDisposables.clear();
|
||||
}
|
||||
};
|
||||
|
||||
reconcileProviderRegistrations();
|
||||
disposableStore.add(this.onDidChangeContentProviderSchemes(({ added, removed }) => {
|
||||
if (added.includes(contribution.type) || removed.includes(contribution.type)) {
|
||||
reconcileProviderRegistrations();
|
||||
}
|
||||
}));
|
||||
|
||||
disposableStore.add(this._registerMenuItems(contribution, ext));
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user