diff --git a/package.json b/package.json index 1d6b2fcdd39..18b9a350724 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.107.0", - "distro": "55e124105c9ac2939053cdc9f50ff7d84fc86294", + "distro": "f7daaf68414ef6e47bec698f8babc297f0d90f0d", "author": { "name": "Microsoft Corporation" }, diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 5329d65bb46..2dc76e8c7ce 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -43,6 +43,7 @@ export interface IChatSessionRecommendation { readonly displayName: string; readonly name: string; readonly description: string; + readonly postInstallCommand?: string; } export type ConfigurationSyncStore = { diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 47feb903105..1da4aa0b405 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -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'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAgentRecommendationActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAgentRecommendationActions.ts new file mode 100644 index 00000000000..9680e885a87 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAgentRecommendationActions.ts @@ -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>(); + 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(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 { + 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 { + if (!commandId) { + return; + } + + await waitForCommandRegistration(commandId); + + try { + await commandService.executeCommand(commandId); + } catch { + // Command failed or was cancelled; ignore. + } +} + +function waitForCommandRegistration(commandId: string): Promise { + if (CommandsRegistry.getCommands().has(commandId)) { + return Promise.resolve(); + } + + return new Promise(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); + }); +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts index 643e978c649..e5a30b2de4f 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatSessionActions.ts @@ -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 { - 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: { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts index c8faf90c729..d4b97b7acdc 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsView.ts @@ -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; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index f13544a8f95..df9e4867104 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -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); diff --git a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts index 9a6c96457d6..c784a04bc76 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts @@ -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(); + 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)); }