Path: blob/main/src/vs/sessions/contrib/accountMenu/browser/account.contribution.ts
13401 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 '../../../browser/media/sidebarActionButton.css';6import './media/accountWidget.css';7import './media/accountTitleBarWidget.css';8import '../../../../workbench/contrib/chat/browser/chatStatus/media/chatStatus.css';9import Severity from '../../../../base/common/severity.js';10import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';11import { localize, localize2 } from '../../../../nls.js';12import { Action2, MenuRegistry, registerAction2, IMenuService } from '../../../../platform/actions/common/actions.js';13import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';14import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';15import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';16import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';17import { appendUpdateMenuItems as registerUpdateMenuItems } from '../../../../workbench/contrib/update/browser/update.js';18import { Menus } from '../../../browser/menus.js';19import { IActionViewItemService } from '../../../../platform/actions/browser/actionViewItemService.js';20import { fillInActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';21import { $, addDisposableListener, append, disposableWindowInterval, EventType, getDomNodePagePosition } from '../../../../base/browser/dom.js';22import { mainWindow } from '../../../../base/browser/window.js';23import { BaseActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';24import { IAction, Separator } from '../../../../base/common/actions.js';25import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';26import { Codicon } from '../../../../base/common/codicons.js';27import { IUpdateService, State, StateType } from '../../../../platform/update/common/update.js';28import { IHoverService } from '../../../../platform/hover/browser/hover.js';29import { IProductService } from '../../../../platform/product/common/productService.js';30import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';31import { IHostService } from '../../../../workbench/services/host/browser/host.js';32import { IOpenerService } from '../../../../platform/opener/common/opener.js';33import { URI } from '../../../../base/common/uri.js';34import { isWindows, isMacintosh } from '../../../../base/common/platform.js';35import { UpdateHoverWidget } from './updateHoverWidget.js';36import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';37import { ChatStatusDashboard, IChatStatusDashboardOptions } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.js';38import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';39import { ThemeIcon } from '../../../../base/common/themables.js';40import { getAccountProfileImageUrl, getAccountTitleBarBadgeKey, getAccountTitleBarState, resolveAccountInfo } from '../../../browser/accountTitleBarState.js';41import { IsPhoneLayoutContext, SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';42import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js';43import { IAuthenticationAccessService } from '../../../../workbench/services/authentication/browser/authenticationAccessService.js';44import { IAuthenticationUsageService } from '../../../../workbench/services/authentication/browser/authenticationUsageService.js';45import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';46import { IChatDashboardService } from '../../../browser/chatDashboardService.js';47import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';4849// --- Account Menu Items --- //50const AccountMenu = Menus.AccountMenu;51const SessionsTitleBarAccountWidgetAction = 'sessions.action.titleBarAccountWidget';52const SessionsTitleBarUpdateWidgetAction = 'sessions.action.titleBarUpdateWidget';53const SESSIONS_ACCOUNT_TITLEBAR_PANEL_WIDTH = 360;5455const PERSONALIZE_ACTION_IDS: readonly string[] = [56'workbench.action.openSettings',57'workbench.action.openGlobalKeybindings',58'workbench.action.selectTheme',59];60const SIGN_OUT_ACTION_ID = 'workbench.action.agenticSignOut';61const SIGN_IN_ACTION_ID = 'workbench.action.agenticSignIn';6263function shouldHideSessionsTitleBarUpdateWidget(type: StateType): boolean {64return type === StateType.Uninitialized65|| type === StateType.Idle66|| type === StateType.Disabled67|| type === StateType.CheckingForUpdates;68}6970function isPrimarySessionsTitleBarUpdateWidget(type: StateType): boolean {71return type === StateType.AvailableForDownload72|| type === StateType.Downloaded73|| type === StateType.Ready;74}7576function isBusySessionsTitleBarUpdateWidget(type: StateType): boolean {77return type === StateType.Downloading78|| type === StateType.Overwriting79|| type === StateType.Updating80|| type === StateType.Restarting;81}8283function getSessionsTitleBarUpdateLabel(state: State): string {84switch (state.type) {85case StateType.AvailableForDownload:86return localize('sessionsTitleBarUpdateAvailable', "Update Available");87case StateType.Downloaded:88return localize('sessionsTitleBarInstallUpdate', "Install Update");89case StateType.Ready:90return localize('sessionsTitleBarRestartToUpdate', "Restart to Update");91case StateType.Downloading:92case StateType.Overwriting:93return localize('sessionsTitleBarDownloading', "Downloading...");94case StateType.Updating:95case StateType.Restarting:96return localize('sessionsTitleBarInstalling', "Installing...");97default:98return localize('sessionsTitleBarUpdate', "Update");99}100}101102function getSessionsTitleBarUpdateAriaLabel(state: State): string {103switch (state.type) {104case StateType.AvailableForDownload:105return localize('sessionsTitleBarUpdateAvailableAria', "Update available");106case StateType.Downloaded:107return localize('sessionsTitleBarInstallUpdateAria', "Install downloaded update");108case StateType.Ready:109return localize('sessionsTitleBarRestartToUpdateAria', "Restart to apply update");110case StateType.Downloading:111case StateType.Overwriting:112return localize('sessionsTitleBarDownloadingAria', "Update download in progress");113case StateType.Updating:114case StateType.Restarting:115return localize('sessionsTitleBarInstallingAria', "Update install in progress");116default:117return localize('sessionsTitleBarUpdateAria', "Update");118}119}120121async function runSessionsUpdateAction(122state: State,123updateService: IUpdateService,124openerService: IOpenerService,125productService: IProductService,126dialogService: IDialogService,127hostService: IHostService,128): Promise<void> {129if (state.type === StateType.AvailableForDownload) {130const isInsiderOrExploration = productService.quality === 'insider' || productService.quality === 'exploration';131const hasCrossAppCoordinator = (isWindows || isMacintosh) && isInsiderOrExploration;132if (!hasCrossAppCoordinator) {133const { confirmed } = await dialogService.confirm({134message: localize('sessionsUpdateFromVSCode.title', "Update from VS Code"),135detail: localize('sessionsUpdateFromVSCode.detail', "This will close the Agents app and open VS Code so you can install the update.\n\nLaunch Agents again after the update is complete."),136primaryButton: localize('sessionsUpdateFromVSCode.open', "Close and Open VS Code"),137});138139if (confirmed) {140await openerService.open(URI.from({141scheme: productService.urlProtocol,142query: 'windowId=_blank',143}), { openExternal: true });144await hostService.shutdown();145}146147return;148}149150await updateService.downloadUpdate(true);151return;152}153154if (state.type === StateType.Ready) {155await updateService.quitAndInstall();156return;157}158159if (state.type === StateType.Downloaded) {160await updateService.applyUpdate();161}162}163164// Sign In (shown when signed out)165registerAction2(class extends Action2 {166constructor() {167super({168id: 'workbench.action.agenticSignIn',169title: localize2('signIn', 'Sign In'),170menu: {171id: AccountMenu,172when: ContextKeyExpr.notEquals('defaultAccountStatus', 'available'),173group: '1_account',174order: 1,175}176});177}178async run(accessor: ServicesAccessor): Promise<void> {179const defaultAccountService = accessor.get(IDefaultAccountService);180await defaultAccountService.signIn();181}182});183184// Sign Out (shown when signed in)185registerAction2(class extends Action2 {186constructor() {187super({188id: 'workbench.action.agenticSignOut',189title: localize2('signOut', 'Sign Out'),190menu: {191id: AccountMenu,192when: ContextKeyExpr.equals('defaultAccountStatus', 'available'),193group: '1_account',194order: 1,195}196});197}198async run(accessor: ServicesAccessor): Promise<void> {199const defaultAccountService = accessor.get(IDefaultAccountService);200const dialogService = accessor.get(IDialogService);201const authenticationService = accessor.get(IAuthenticationService);202const authenticationUsageService = accessor.get(IAuthenticationUsageService);203const authenticationAccessService = accessor.get(IAuthenticationAccessService);204const defaultAccount = await defaultAccountService.getDefaultAccount();205if (!defaultAccount) {206return;207}208209const providerId = defaultAccount.authenticationProvider.id;210const accountLabel = defaultAccount.accountName;211const { confirmed } = await dialogService.confirm({212type: Severity.Info,213message: localize('agenticSignOutMessage', "Sign out of the Agents app?"),214detail: localize('agenticSignOutDetail', "This will sign out '{0}' from the Agents app.", accountLabel),215primaryButton: localize({ key: 'agenticSignOutButton', comment: ['&& denotes a mnemonic'] }, "&&Sign Out")216});217218if (!confirmed) {219return;220}221222const allSessions = await authenticationService.getSessions(providerId);223const sessions = allSessions.filter(session => session.account.label === accountLabel);224await Promise.all(sessions.map(session => authenticationService.removeSession(providerId, session.id)));225authenticationUsageService.removeAccountUsage(providerId, accountLabel);226authenticationAccessService.removeAllowedExtensions(providerId, accountLabel);227}228});229230// Color Theme (hidden on phone — no theme picker UI on mobile)231MenuRegistry.appendMenuItem(AccountMenu, {232command: {233id: 'workbench.action.selectTheme',234title: localize('selectColorTheme', "Color Theme"),235},236when: IsPhoneLayoutContext.negate(),237group: '2_settings',238order: 1,239});240241// Settings (hidden on phone — no settings UI on mobile)242MenuRegistry.appendMenuItem(AccountMenu, {243command: {244id: 'workbench.action.openSettings',245title: localize('settings', "Settings"),246},247when: IsPhoneLayoutContext.negate(),248group: '2_settings',249order: 2,250});251252// Keyboard Shortcuts (hidden on phone — no keybindings UI on mobile)253MenuRegistry.appendMenuItem(AccountMenu, {254command: {255id: 'workbench.action.openGlobalKeybindings',256title: localize('sessionsAccountMenu.keyboardShortcuts', "Keyboard Shortcuts"),257},258when: IsPhoneLayoutContext.negate(),259group: '2_settings',260order: 3,261});262263// Update actions264registerUpdateMenuItems(AccountMenu, '3_updates');265266class TitleBarAccountWidget extends BaseActionViewItem {267268private container: HTMLElement | undefined;269private avatarElement: HTMLImageElement | undefined;270private iconElement: HTMLElement | undefined;271private labelElement: HTMLElement | undefined;272private badgeElement: HTMLElement | undefined;273private accountName: string | undefined;274private accountProviderId: string | undefined;275private accountProviderLabel: string | undefined;276private isAccountLoading = true;277private accountRequestCounter = 0;278private avatarRequestCounter = 0;279private currentAvatarUrl: string | undefined;280private loadedAvatarUrl: string | undefined;281private lastState: ReturnType<typeof getAccountTitleBarState>;282private isMenuVisible = false;283private lastBadgeKey: string | undefined;284private dismissedBadgeKey: string | undefined;285private readonly copilotDashboardStore = this._register(new MutableDisposable<DisposableStore>());286private readonly clickPanelDisposable = this._register(new MutableDisposable<DisposableStore>());287private readonly avatarLoadDisposable = this._register(new MutableDisposable());288289constructor(290action: IAction,291options: IBaseActionViewItemOptions | undefined,292@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,293@IAuthenticationService private readonly authenticationService: IAuthenticationService,294@IMenuService private readonly menuService: IMenuService,295@IContextKeyService private readonly contextKeyService: IContextKeyService,296@IHoverService private readonly hoverService: IHoverService,297@IInstantiationService private readonly instantiationService: IInstantiationService,298@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,299) {300super(undefined, action, options);301this.lastState = getAccountTitleBarState({302isAccountLoading: true,303entitlement: this.chatEntitlementService.entitlement,304sentiment: this.chatEntitlementService.sentiment,305quotas: this.chatEntitlementService.quotas,306});307308this._register(this.defaultAccountService.onDidChangeDefaultAccount(() => this.refreshAccount()));309this._register(this.authenticationService.onDidChangeSessions(() => this.refreshAccount()));310this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.renderState()));311this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.renderState()));312this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.renderState()));313this._register(this.chatEntitlementService.onDidChangeQuotaRemaining(() => this.renderState()));314this.refreshAccount();315}316317override setFocusable(_focusable: boolean): void {318// Don't let the ActionBar remove focusability - this widget must319// always be reachable via Tab even when a sibling item is hidden.320}321322override render(container: HTMLElement): void {323super.render(container);324325this.container = container;326container.classList.add('sessions-account-titlebar-widget');327container.setAttribute('role', 'button');328container.tabIndex = 0;329330this.avatarElement = append(container, $('img.sessions-account-titlebar-widget-avatar', { alt: localize('accountAvatarAltFallback', "Account profile image"), draggable: 'false' })) as HTMLImageElement;331this.avatarElement.decoding = 'async';332this.avatarElement.referrerPolicy = 'no-referrer';333this.iconElement = append(container, $('.sessions-account-titlebar-widget-icon'));334this.labelElement = append(container, $('span.sessions-account-titlebar-widget-label'));335this.badgeElement = append(container, $('span.sessions-account-titlebar-widget-badge'));336337this.renderState();338}339340override onClick(): void {341if (!this.container) {342return;343}344345this.showCombinedPanel();346}347348private async refreshAccount(): Promise<void> {349const requestId = ++this.accountRequestCounter;350this.isAccountLoading = true;351this.renderState();352353const info = await resolveAccountInfo(this.defaultAccountService, this.authenticationService);354if (requestId !== this.accountRequestCounter) {355return;356}357358this.accountName = info?.accountName;359this.accountProviderId = info?.accountProviderId;360this.accountProviderLabel = info?.accountProviderLabel;361this.isAccountLoading = false;362this.refreshAvatar();363this.renderState();364}365366private renderState(): void {367if (!this.container || !this.avatarElement || !this.iconElement || !this.labelElement || !this.badgeElement) {368return;369}370371// When we have a session but entitlement hasn't resolved yet,372// treat as Unresolved to avoid showing "Agents Signed Out".373const entitlement = this.accountName && this.chatEntitlementService.entitlement === ChatEntitlement.Unknown374? ChatEntitlement.Unresolved375: this.chatEntitlementService.entitlement;376377const state = getAccountTitleBarState({378isAccountLoading: this.isAccountLoading,379accountName: this.accountName,380accountProviderLabel: this.accountProviderLabel,381entitlement,382sentiment: this.chatEntitlementService.sentiment,383quotas: this.chatEntitlementService.quotas,384});385this.lastState = state;386387this.container.classList.remove('kind-default', 'kind-accent', 'kind-warning', 'kind-prominent');388this.container.classList.add(`kind-${state.kind}`);389this.container.classList.toggle('menu-visible', this.isMenuVisible);390this.container.setAttribute('aria-label', state.ariaLabel);391392const badgeKey = getAccountTitleBarBadgeKey(state);393if (badgeKey !== this.lastBadgeKey) {394this.lastBadgeKey = badgeKey;395this.dismissedBadgeKey = undefined;396}397398const shouldShowDotBadge = !!badgeKey && badgeKey !== this.dismissedBadgeKey;399const loadedAvatarUrl = !this.isAccountLoading ? this.loadedAvatarUrl : undefined;400const hasLoadedAvatar = !!loadedAvatarUrl;401const titleBarIcon = state.dotBadge ? Codicon.account : state.icon;402403this.avatarElement.classList.toggle('visible', hasLoadedAvatar);404this.avatarElement.alt = this.getAvatarAltText(hasLoadedAvatar);405if (hasLoadedAvatar) {406if (this.avatarElement.src !== loadedAvatarUrl) {407this.avatarElement.src = loadedAvatarUrl;408}409} else {410this.avatarElement.removeAttribute('src');411}412413this.iconElement.className = `sessions-account-titlebar-widget-icon ${ThemeIcon.asClassName(titleBarIcon)}`;414this.iconElement.classList.toggle('hidden', hasLoadedAvatar);415this.labelElement.textContent = '';416this.badgeElement.textContent = '';417this.badgeElement.classList.toggle('dot-badge', shouldShowDotBadge);418this.badgeElement.classList.toggle('dot-badge-warning', shouldShowDotBadge && state.dotBadge === 'warning');419this.badgeElement.classList.toggle('dot-badge-error', shouldShowDotBadge && state.dotBadge === 'error');420this.badgeElement.style.display = shouldShowDotBadge ? '' : 'none';421}422423private getAvatarAltText(hasLoadedAvatar: boolean): string {424if (hasLoadedAvatar && this.accountProviderId === 'github' && this.accountName) {425return localize('accountAvatarAlt', "GitHub profile image for {0}", this.accountName);426}427428return localize('accountAvatarAltFallback', "Account profile image");429}430431private refreshAvatar(): void {432const avatarUrl = getAccountProfileImageUrl(this.accountProviderId, this.accountName);433if (avatarUrl === this.currentAvatarUrl) {434return;435}436437this.currentAvatarUrl = avatarUrl;438this.loadedAvatarUrl = undefined;439this.avatarLoadDisposable.clear();440const requestId = ++this.avatarRequestCounter;441442if (!avatarUrl) {443this.renderState();444return;445}446447const image = new Image();448image.referrerPolicy = 'no-referrer';449const clearHandlers = () => {450image.onload = null;451image.onerror = null;452};453image.onload = () => {454if (requestId !== this.avatarRequestCounter) {455return;456}457458this.loadedAvatarUrl = avatarUrl;459this.renderState();460clearHandlers();461};462image.onerror = () => {463if (requestId !== this.avatarRequestCounter) {464return;465}466467this.loadedAvatarUrl = undefined;468this.renderState();469clearHandlers();470};471this.avatarLoadDisposable.value = toDisposable(() => {472clearHandlers();473image.src = '';474});475image.src = avatarUrl;476this.renderState();477}478479private getHoverTarget(): { targetElements: HTMLElement[]; x: number } {480const { left, width } = getDomNodePagePosition(this.container!);481return {482targetElements: [this.container!],483x: left + width - SESSIONS_ACCOUNT_TITLEBAR_PANEL_WIDTH,484};485}486487private showCombinedPanel(): void {488if (!this.container) {489return;490}491492if (this.isMenuVisible) {493this.hoverService.hideHover(true);494this.clickPanelDisposable.clear();495return;496}497498this.hoverService.hideHover(true);499this.clickPanelDisposable.clear();500501const panelStore = new DisposableStore();502this.clickPanelDisposable.value = panelStore;503504const badgeKey = getAccountTitleBarBadgeKey(this.lastState);505if (badgeKey) {506this.dismissedBadgeKey = badgeKey;507}508509this.isMenuVisible = true;510this.container.classList.add('menu-visible');511this.renderState();512513panelStore.add({514dispose: () => {515this.isMenuVisible = false;516this.container?.classList.remove('menu-visible');517this.renderState();518this.container?.focus();519}520});521522const panelContent = this.createCombinedPanelContent(panelStore);523const hoverWidget = this.hoverService.showInstantHover({524content: panelContent,525target: this.getHoverTarget(),526additionalClasses: ['sessions-account-titlebar-panel-hover'],527position: { hoverPosition: HoverPosition.BELOW },528persistence: { sticky: true, hideOnHover: false },529appearance: { showPointer: false, skipFadeInAnimation: true, maxHeightRatio: 0.8 },530}, true);531532if (hoverWidget) {533panelStore.add(hoverWidget);534}535536panelStore.add(disposableWindowInterval(mainWindow, () => {537if (!panelContent.isConnected || hoverWidget?.isDisposed) {538this.clickPanelDisposable.clear();539}540}, 500));541}542543private createCombinedPanelContent(panelStore: DisposableStore): HTMLElement {544const panel = $('div.sessions-account-titlebar-panel');545546// Build the menu actions once and partition them.547const menu = this.menuService.createMenu(AccountMenu, this.contextKeyService);548const rawActions: IAction[] = [];549fillInActionBarActions(menu.getActions(), rawActions);550menu.dispose();551const partitioned = this.partitionMenuActions(rawActions);552553// Header: account label + sign-out icon.554const headerSection = append(panel, $('.sessions-account-titlebar-panel-header'));555const loadedAvatarUrl = !this.isAccountLoading ? this.loadedAvatarUrl : undefined;556if (loadedAvatarUrl) {557const avatar = append(headerSection, $('img.sessions-account-titlebar-panel-avatar', {558alt: this.getAvatarAltText(true),559draggable: 'false',560src: loadedAvatarUrl,561})) as HTMLImageElement;562avatar.decoding = 'async';563avatar.referrerPolicy = 'no-referrer';564}565const title = append(headerSection, $('div.sessions-account-titlebar-panel-title'));566title.textContent = this.getPanelHeaderLabel();567if (partitioned.signOut) {568const headerActionsContainer = append(headerSection, $('.sessions-account-titlebar-panel-header-actions'));569this.createPanelButton(headerActionsContainer, partitioned.signOut, panelStore, {570classNames: ['sessions-account-titlebar-panel-header-action'],571icon: this.getHeaderActionIcon(partitioned.signOut),572});573}574575// Personalize section.576if (partitioned.personalize.length > 0) {577const personalizeId = 'sessions-account-personalize-title';578const personalizeSection = append(panel, $('section.sessions-account-titlebar-panel-section', { 'aria-labelledby': personalizeId }));579const personalizeHeading = append(personalizeSection, $('div.sessions-account-titlebar-panel-section-title', { id: personalizeId }));580personalizeHeading.textContent = localize('sessionsAccountMenu.personalize', "Personalize");581const personalizeActionsContainer = append(personalizeSection, $('.sessions-account-titlebar-panel-actions'));582for (const action of partitioned.personalize) {583this.createPanelButton(personalizeActionsContainer, action, panelStore, {584classNames: ['sessions-account-titlebar-panel-action', 'with-icon'],585icon: this.getPersonalizeActionIcon(action),586includeLabel: true,587});588}589}590591// Other panel actions (sign-in, etc.) — only render if there's at least one non-separator action.592if (partitioned.other.some(a => !(a instanceof Separator))) {593const actionsSection = append(panel, $('.sessions-account-titlebar-panel-actions'));594let lastWasSeparator = true;595for (const action of partitioned.other) {596if (action instanceof Separator) {597if (!lastWasSeparator) {598append(actionsSection, $('.sessions-account-titlebar-panel-separator'));599lastWasSeparator = true;600}601continue;602}603lastWasSeparator = false;604this.createPanelButton(actionsSection, action, panelStore, {605classNames: ['sessions-account-titlebar-panel-action'],606includeLabel: true,607checked: !!action.checked,608});609}610}611612// Subscription / Copilot dashboard.613const contentSection = append(panel, $('.sessions-account-titlebar-panel-content'));614if (this.shouldShowCopilotDashboardHover()) {615const subscriptionId = 'sessions-account-subscription-title';616const subscriptionSection = append(contentSection, $('section.sessions-account-titlebar-panel-section.subscription', { 'aria-labelledby': subscriptionId }));617const subscriptionHeader = append(subscriptionSection, $('.sessions-account-titlebar-panel-section-header'));618const subscriptionHeading = append(subscriptionHeader, $('div.sessions-account-titlebar-panel-section-title', { id: subscriptionId }));619subscriptionHeading.textContent = localize('sessionsAccountMenu.subscription', "Subscription");620// Render the dashboard's title header (plan name + manage / CTA actions)621// directly into our section header row via the dashboard's public API.622const dashboard = this.createCopilotHoverContent({ titleHeaderContainer: subscriptionHeader });623append(subscriptionSection, dashboard);624} else if (!this.isAccountLoading) {625const summary = append(contentSection, $('.sessions-account-titlebar-panel-summary'));626summary.textContent = this.lastState.ariaLabel;627}628629return panel;630}631632private partitionMenuActions(rawActions: IAction[]): { signOut: IAction | undefined; personalize: IAction[]; other: IAction[] } {633let signOut: IAction | undefined;634const personalizeMap = new Map<string, IAction>();635const other: IAction[] = [];636637const pushSeparator = () => {638// Collapse runs and skip leading separators so groups whose only639// items get filtered (e.g. update.*) don't leave orphans behind.640if (other.length === 0 || other[other.length - 1] instanceof Separator) {641return;642}643other.push(new Separator());644};645646for (const action of rawActions) {647if (action instanceof Separator) {648pushSeparator();649continue;650}651if (action.id === SIGN_OUT_ACTION_ID) {652signOut = action;653continue;654}655if (PERSONALIZE_ACTION_IDS.includes(action.id)) {656personalizeMap.set(action.id, action);657continue;658}659if (action.id.startsWith('update.')) {660continue;661}662if (this.isAccountLoading && action.id === SIGN_IN_ACTION_ID) {663continue;664}665other.push(action);666}667668// Trim trailing separator left after filtering.669if (other.length > 0 && other[other.length - 1] instanceof Separator) {670other.pop();671}672673// Preserve canonical personalize order.674const personalize = PERSONALIZE_ACTION_IDS675.map(id => personalizeMap.get(id))676.filter((a): a is IAction => !!a);677678return { signOut, personalize, other };679}680681private createPanelButton(682parent: HTMLElement,683action: IAction,684panelStore: DisposableStore,685options: { classNames: readonly string[]; icon?: ThemeIcon; includeLabel?: boolean; checked?: boolean },686): HTMLButtonElement {687const button = append(parent, $('button', { type: 'button' })) as HTMLButtonElement;688button.classList.add(...options.classNames);689button.disabled = !action.enabled;690button.setAttribute('aria-label', action.tooltip || action.label);691if (options.checked) {692button.classList.add('checked');693}694695if (options.icon && options.includeLabel) {696const iconElement = append(button, $('span.sessions-account-titlebar-panel-action-icon'));697iconElement.classList.add(...ThemeIcon.asClassNameArray(options.icon));698const labelElement = append(button, $('span.sessions-account-titlebar-panel-action-label'));699append(labelElement, ...renderLabelWithIcons(action.label));700} else if (options.icon) {701button.title = action.tooltip || action.label;702button.classList.add(...ThemeIcon.asClassNameArray(options.icon));703} else {704append(button, ...renderLabelWithIcons(action.label));705}706707panelStore.add(addDisposableListener(button, EventType.CLICK, async event => {708event.preventDefault();709event.stopPropagation();710this.hoverService.hideHover(true);711this.clickPanelDisposable.clear();712await Promise.resolve(action.run());713}));714715return button;716}717718private getPanelHeaderLabel(): string {719if (this.accountName) {720return this.accountName;721}722723if (this.isAccountLoading) {724return localize('loadingAccountHeader', "Loading Account...");725}726727return localize('accountMenuHeaderFallback', "Account");728}729730private getHeaderActionIcon(action: IAction): ThemeIcon {731switch (action.id) {732case 'workbench.action.selectTheme':733return Codicon.symbolColor;734case 'workbench.action.openSettings':735return Codicon.settingsGear;736case SIGN_OUT_ACTION_ID:737return Codicon.signOut;738default:739return Codicon.circleLargeFilled;740}741}742743private getPersonalizeActionIcon(action: IAction): ThemeIcon {744switch (action.id) {745case 'workbench.action.openSettings':746return Codicon.settingsGear;747case 'workbench.action.openGlobalKeybindings':748return Codicon.keyboard;749case 'workbench.action.selectTheme':750return Codicon.symbolColor;751default:752return Codicon.circleLargeFilled;753}754}755756private shouldShowCopilotDashboardHover(): boolean {757return !this.chatEntitlementService.sentiment.hidden && !!this.accountName;758}759760private createCopilotHoverContent(extraOptions?: Partial<IChatStatusDashboardOptions>): HTMLElement {761const store = new DisposableStore();762this.copilotDashboardStore.value = store;763const dashboardElement = ChatStatusDashboard.instantiateInContents(this.instantiationService, store, {764disableInlineSuggestionsSettings: true,765disableModelSelection: true,766disableProviderOptions: true,767disableCompletionsSnooze: true,768disableQuickSettingsCollapsible: true,769disableContributedSectionsCollapsible: true,770...extraOptions,771});772773store.add(disposableWindowInterval(mainWindow, () => {774if (!dashboardElement.isConnected) {775store.dispose();776}777}, 2000));778779return dashboardElement;780}781}782783class TitleBarUpdateWidget extends BaseActionViewItem {784785private container: HTMLElement | undefined;786private labelElement: HTMLElement | undefined;787private readonly updateHoverWidget: UpdateHoverWidget;788private readonly hoverAttachment = this._register(new MutableDisposable());789790constructor(791action: IAction,792options: IBaseActionViewItemOptions | undefined,793@IUpdateService private readonly updateService: IUpdateService,794@IHoverService private readonly hoverService: IHoverService,795@IProductService private readonly productService: IProductService,796@IOpenerService private readonly openerService: IOpenerService,797@IDialogService private readonly dialogService: IDialogService,798@IHostService private readonly hostService: IHostService,799) {800super(undefined, action, options);801this.updateHoverWidget = new UpdateHoverWidget(this.updateService, this.productService, this.hoverService);802this._register(this.updateService.onStateChange(() => this.renderState()));803}804805override render(container: HTMLElement): void {806super.render(container);807808this.container = container;809container.classList.add('sessions-update-titlebar-widget');810container.setAttribute('role', 'button');811812this.labelElement = append(container, $('span.sessions-update-titlebar-widget-label'));813this.hoverAttachment.value = this.updateHoverWidget.attachTo(container);814815this.renderState();816}817818override onClick(): void {819const state = this.updateService.state;820if (shouldHideSessionsTitleBarUpdateWidget(state.type) || isBusySessionsTitleBarUpdateWidget(state.type)) {821return;822}823824void runSessionsUpdateAction(825state,826this.updateService,827this.openerService,828this.productService,829this.dialogService,830this.hostService,831);832}833834private renderState(): void {835if (!this.container || !this.labelElement) {836return;837}838839const state = this.updateService.state;840const hidden = shouldHideSessionsTitleBarUpdateWidget(state.type);841const busy = isBusySessionsTitleBarUpdateWidget(state.type);842const primary = isPrimarySessionsTitleBarUpdateWidget(state.type);843844this.container.classList.toggle('hidden', hidden);845this.container.classList.toggle('disabled', busy);846this.container.classList.toggle('primary-state', primary);847this.container.classList.toggle('busy-state', busy);848849if (hidden) {850this.container.removeAttribute('aria-label');851this.labelElement.textContent = '';852return;853}854855this.container.setAttribute('aria-label', getSessionsTitleBarUpdateAriaLabel(state));856this.labelElement.textContent = getSessionsTitleBarUpdateLabel(state);857}858}859860// --- Register custom view item --- //861862// Actions registered at module level so Menus.TitleBarRightLayout is non-empty when the863// toolbar is first constructed. The run() is a no-op — rendering is handled by the custom864// view items registered in AccountWidgetContribution.865registerAction2(class extends Action2 {866constructor() {867super({868id: SessionsTitleBarUpdateWidgetAction,869title: localize2('agentsUpdateTitleBar', "Agents Update"),870menu: {871id: Menus.TitleBarRightLayout,872group: 'navigation',873order: 99,874when: ContextKeyExpr.and(IsAuxiliaryWindowContext.toNegated(), SessionsWelcomeVisibleContext.toNegated()),875}876});877}878879run(): void { }880});881882registerAction2(class extends Action2 {883constructor() {884super({885id: SessionsTitleBarAccountWidgetAction,886title: localize2('agentsAccountStatusTitleBar', "Agents Account and Status"),887menu: {888id: Menus.TitleBarRightLayout,889group: 'navigation',890order: 100,891when: IsAuxiliaryWindowContext.toNegated(),892}893});894}895896run(): void { }897});898899class AccountWidgetContribution extends Disposable implements IWorkbenchContribution {900901static readonly ID = 'workbench.contrib.sessionsWidget';902903constructor(904@IActionViewItemService actionViewItemService: IActionViewItemService,905@IInstantiationService instantiationService: IInstantiationService,906) {907super();908909this._register(actionViewItemService.register(Menus.TitleBarRightLayout, SessionsTitleBarUpdateWidgetAction, (action, options) => {910return instantiationService.createInstance(TitleBarUpdateWidget, action, options);911}, undefined));912913this._register(actionViewItemService.register(Menus.TitleBarRightLayout, SessionsTitleBarAccountWidgetAction, (action, options) => {914return instantiationService.createInstance(TitleBarAccountWidget, action, options);915}, undefined));916}917}918919registerWorkbenchContribution2(AccountWidgetContribution.ID, AccountWidgetContribution, WorkbenchPhase.BlockRestore);920921// --- Chat Dashboard Service (real implementation for mobile account sheet) --- //922923class ChatDashboardServiceImpl implements IChatDashboardService {924readonly _serviceBrand: undefined;925926constructor(927@IInstantiationService private readonly instantiationService: IInstantiationService,928) { }929930createDashboardElement(store: DisposableStore): HTMLElement | undefined {931const dashboardElement = ChatStatusDashboard.instantiateInContents(this.instantiationService, store, {932disableInlineSuggestionsSettings: true,933disableModelSelection: true,934disableProviderOptions: true,935disableCompletionsSnooze: true,936});937938store.add(disposableWindowInterval(mainWindow, () => {939if (!dashboardElement.isConnected) {940store.dispose();941}942}, 2000));943944return dashboardElement;945}946}947948registerSingleton(IChatDashboardService, ChatDashboardServiceImpl, InstantiationType.Delayed);949950951