From 04da0048f79de7692e66390dae58c4aa392c61bd Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 20 Oct 2025 14:37:49 -0700 Subject: [PATCH] Add Manage Accounts command and UI (#272042) * Add Manage Accounts command and UI Fix for #233881 * Update src/vs/workbench/contrib/authentication/browser/actions/manageAccountsAction.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/actions/manageAccountsAction.ts | 140 ++++++++++++++++++ .../browser/authentication.contribution.ts | 2 + 2 files changed, 142 insertions(+) create mode 100644 src/vs/workbench/contrib/authentication/browser/actions/manageAccountsAction.ts diff --git a/src/vs/workbench/contrib/authentication/browser/actions/manageAccountsAction.ts b/src/vs/workbench/contrib/authentication/browser/actions/manageAccountsAction.ts new file mode 100644 index 00000000000..27a84b0e281 --- /dev/null +++ b/src/vs/workbench/contrib/authentication/browser/actions/manageAccountsAction.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Lazy } from '../../../../../base/common/lazy.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2 } from '../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; +import { ISecretStorageService } from '../../../../../platform/secrets/common/secrets.js'; +import { getCurrentAuthenticationSessionInfo } from '../../../../services/authentication/browser/authenticationService.js'; +import { IAuthenticationProvider, IAuthenticationService } from '../../../../services/authentication/common/authentication.js'; + +export class ManageAccountsAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.manageAccounts', + title: localize2('manageAccounts', "Manage Accounts"), + category: localize2('accounts', "Accounts"), + f1: true + }); + } + + public override run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + return instantiationService.createInstance(ManageAccountsActionImpl).run(); + } +} + +interface AccountQuickPickItem extends IQuickPickItem { + providerId: string; + canUseMcp: boolean; + canSignOut: () => Promise; +} + +interface AccountActionQuickPickItem extends IQuickPickItem { + action: () => void; +} + +class ManageAccountsActionImpl { + private activeSession = new Lazy(() => getCurrentAuthenticationSessionInfo(this.secretStorageService, this.productService)); + + constructor( + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IAuthenticationService private readonly authenticationService: IAuthenticationService, + @ICommandService private readonly commandService: ICommandService, + @ISecretStorageService private readonly secretStorageService: ISecretStorageService, + @IProductService private readonly productService: IProductService, + ) { } + + public async run() { + const placeHolder = localize('pickAccount', "Select an account to manage"); + + const accounts = await this.listAccounts(); + if (!accounts.length) { + await this.quickInputService.pick([{ label: localize('noActiveAccounts', "There are no active accounts.") }], { placeHolder }); + return; + } + + const account = await this.quickInputService.pick(accounts, { placeHolder, matchOnDescription: true }); + if (!account) { + return; + } + + await this.showAccountActions(account); + } + + private async listAccounts(): Promise { + const accounts: AccountQuickPickItem[] = []; + for (const providerId of this.authenticationService.getProviderIds()) { + const provider = this.authenticationService.getProvider(providerId); + for (const { label, id } of await this.authenticationService.getAccounts(providerId)) { + accounts.push({ + label, + description: provider.label, + providerId, + canUseMcp: !!provider.authorizationServers?.length, + canSignOut: () => this.canSignOut(provider, id) + }); + } + } + return accounts; + } + + private async canSignOut(provider: IAuthenticationProvider, accountId: string): Promise { + const session = await this.activeSession.value; + if (session && !session.canSignOut && session.providerId === provider.id) { + const sessions = await this.authenticationService.getSessions(provider.id); + return !sessions.some(o => o.id === session.id && o.account.id === accountId); + } + return true; + } + + private async showAccountActions(account: AccountQuickPickItem): Promise { + const { providerId, label: accountLabel, canUseMcp, canSignOut } = account; + + const store = new DisposableStore(); + const quickPick = store.add(this.quickInputService.createQuickPick()); + + quickPick.title = localize('manageAccount', "Manage '{0}'", accountLabel); + quickPick.placeholder = localize('selectAction', "Select an action"); + + const items: AccountActionQuickPickItem[] = [{ + label: localize('manageTrustedExtensions', "Manage Trusted Extensions"), + action: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel }) + }]; + + if (canUseMcp) { + items.push({ + label: localize('manageTrustedMCPServers', "Manage Trusted MCP Servers"), + action: () => this.commandService.executeCommand('_manageTrustedMCPServersForAccount', { providerId, accountLabel }) + }); + } + + if (await canSignOut()) { + items.push({ + label: localize('signOut', "Sign Out"), + action: () => this.commandService.executeCommand('_signOutOfAccount', { providerId, accountLabel }) + }); + } + + quickPick.items = items; + + store.add(quickPick.onDidAccept(() => { + const selected = quickPick.selectedItems[0]; + if (selected) { + quickPick.hide(); + selected.action(); + } + })); + + store.add(quickPick.onDidHide(() => store.dispose())); + + quickPick.show(); + } +} diff --git a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts index 693975bc45a..e13ee49b07a 100644 --- a/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts +++ b/src/vs/workbench/contrib/authentication/browser/authentication.contribution.ts @@ -20,6 +20,7 @@ import { IAuthenticationUsageService } from '../../../services/authentication/br import { ManageAccountPreferencesForMcpServerAction } from './actions/manageAccountPreferencesForMcpServerAction.js'; import { ManageTrustedMcpServersForAccountAction } from './actions/manageTrustedMcpServersForAccountAction.js'; import { RemoveDynamicAuthenticationProvidersAction } from './actions/manageDynamicAuthenticationProvidersAction.js'; +import { ManageAccountsAction } from './actions/manageAccountsAction.js'; import { IAuthenticationQueryService } from '../../../services/authentication/common/authenticationQuery.js'; import { IMcpRegistry } from '../../mcp/common/mcpRegistryTypes.js'; import { autorun } from '../../../../base/common/observable.js'; @@ -92,6 +93,7 @@ class AuthenticationContribution extends Disposable implements IWorkbenchContrib } private _registerActions(): void { + this._register(registerAction2(ManageAccountsAction)); this._register(registerAction2(SignOutOfAccountAction)); this._register(registerAction2(ManageTrustedExtensionsForAccountAction)); this._register(registerAction2(ManageAccountPreferencesForExtensionAction));