Path: blob/main/src/vs/sessions/browser/parts/mobile/mobileTitlebarPart.ts
13399 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 './mobileChatShell.css';6import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { $, addDisposableListener, append, EventType } from '../../../../base/browser/dom.js';8import { Emitter, Event } from '../../../../base/common/event.js';9import { ThemeIcon } from '../../../../base/common/themables.js';10import { Codicon } from '../../../../base/common/codicons.js';11import { IAction, Separator } from '../../../../base/common/actions.js';12import { localize } from '../../../../nls.js';13import { autorun } from '../../../../base/common/observable.js';14import { URI } from '../../../../base/common/uri.js';15import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';16import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';17import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';18import { IMenuService } from '../../../../platform/actions/common/actions.js';19import { fillInActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';20import { ICommandService } from '../../../../platform/commands/common/commands.js';21import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';22import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';23import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';24import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';25import { ISessionFileChange } from '../../../services/sessions/common/session.js';26import { IsNewChatSessionContext } from '../../../common/contextkeys.js';27import { SideBarVisibleContext } from '../../../../workbench/common/contextkeys.js';28import { Menus } from '../../menus.js';29import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';30import { getAccountTitleBarState, getAccountProfileImageUrl, getAccountTitleBarBadgeKey, resolveAccountInfo } from '../../accountTitleBarState.js';31import { IChatDashboardService } from '../../chatDashboardService.js';32import { basename } from '../../../../base/common/resources.js';33import { IFileDiffViewData, MOBILE_OPEN_DIFF_VIEW_COMMAND_ID } from './contributions/mobileDiffView.js';3435/**36* Mobile titlebar — prepended above the workbench grid on phone viewports37* in place of the desktop titlebar.38*39* Layout (contextual right slot):40*41* - **In a chat session** → `[toggle sidebar] [session title] [changes pill] [+]`42* - **Welcome / new session** → `[toggle sidebar] [host widget | title] [account]`43*44* The center slot switches content based on whether the sessions welcome45* (home/empty) screen is visible:46*47* - **Welcome hidden** → shows the active session title (live, from48* {@link ISessionsManagementService.activeSession}).49* - **Welcome visible** → shows whatever is contributed to the50* {@link Menus.MobileTitleBarCenter} menu. On web, the host filter51* contribution appends its host dropdown + connection button there.52*53* The switch is driven entirely by the menu: when the toolbar has no54* items the title is shown; as soon as it has items the title is hidden55* and the toolbar fills the slot.56*57* The right slot swaps between the new-session (+) button (in a chat)58* and the account indicator (on welcome / new session). The account59* indicator shows the user's avatar or a person icon with an optional60* dot badge for quota/status warnings. Tapping it opens a panel with61* account info, copilot status dashboard, and sign-in/sign-out actions.62*/63export class MobileTitlebarPart extends Disposable {6465readonly element: HTMLElement;6667private readonly sessionTitleElement: HTMLElement;68private readonly actionsContainer: HTMLElement;6970private readonly _onDidClickHamburger = this._register(new Emitter<void>());71readonly onDidClickHamburger: Event<void> = this._onDidClickHamburger.event;7273private readonly _onDidClickNewSession = this._register(new Emitter<void>());74readonly onDidClickNewSession: Event<void> = this._onDidClickNewSession.event;7576private readonly _onDidClickTitle = this._register(new Emitter<void>());77readonly onDidClickTitle: Event<void> = this._onDidClickTitle.event;7879// Account indicator state80private readonly accountButton: HTMLElement;81private readonly accountAvatarElement: HTMLImageElement;82private readonly accountIconElement: HTMLElement;83private readonly accountBadgeElement: HTMLElement;84private accountName: string | undefined;85private accountProviderId: string | undefined;86private accountProviderLabel: string | undefined;87private isAccountLoading = true;88private accountRequestCounter = 0;89private avatarRequestCounter = 0;90private currentAvatarUrl: string | undefined;91private loadedAvatarUrl: string | undefined;92private isAccountMenuVisible = false;93private lastBadgeKey: string | undefined;94private dismissedBadgeKey: string | undefined;95private readonly accountPanelDisposable = this._register(new MutableDisposable<DisposableStore>());96private readonly avatarLoadDisposable = this._register(new MutableDisposable());97private readonly copilotDashboardStore = this._register(new MutableDisposable<DisposableStore>());9899// Changes pill state — kept here so the click handler can read the100// latest set without re-deriving it on each tap.101private latestChanges: readonly ISessionFileChange[] = [];102103constructor(104parent: HTMLElement,105@IInstantiationService instantiationService: IInstantiationService,106@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,107@IContextKeyService private readonly contextKeyService: IContextKeyService,108@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,109@IAuthenticationService private readonly authenticationService: IAuthenticationService,110@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,111@IMenuService private readonly menuService: IMenuService,112@IChatDashboardService private readonly chatDashboardService: IChatDashboardService,113@ICommandService private readonly commandService: ICommandService,114@IQuickInputService private readonly quickInputService: IQuickInputService,115) {116super();117118this.element = document.createElement('div');119this.element.className = 'mobile-top-bar';120121// Register DOM removal before appending so that any exception122// between this point and the end of the constructor still cleans123// up the element via disposal.124this._register(toDisposable(() => this.element.remove()));125parent.prepend(this.element);126127// Sidebar toggle button. Uses the same icon as the desktop/web128// agents-app sidebar toggle and reflects open/closed state via the129// SideBarVisibleContext key.130const hamburger = append(this.element, $('button.mobile-top-bar-button'));131hamburger.setAttribute('aria-label', localize('mobileTopBar.openSessions', "Open sessions"));132const hamburgerIcon = append(hamburger, $('span'));133const closedIconClasses = ThemeIcon.asClassNameArray(Codicon.layoutSidebarLeftOff);134const openIconClasses = ThemeIcon.asClassNameArray(Codicon.layoutSidebarLeft);135hamburgerIcon.classList.add(...closedIconClasses);136this._register(addDisposableListener(hamburger, EventType.CLICK, () => this._onDidClickHamburger.fire()));137138const sidebarVisibleKeySet = new Set([SideBarVisibleContext.key]);139const updateSidebarIcon = () => {140const isOpen = !!SideBarVisibleContext.getValue(contextKeyService);141hamburgerIcon.classList.remove(...closedIconClasses, ...openIconClasses);142hamburgerIcon.classList.add(...(isOpen ? openIconClasses : closedIconClasses));143hamburger.setAttribute('aria-label', isOpen144? localize('mobileTopBar.closeSessions', "Close sessions")145: localize('mobileTopBar.openSessions', "Open sessions"));146};147updateSidebarIcon();148149// Center slot: title and/or actions container (mutually exclusive)150const center = append(this.element, $('div.mobile-top-bar-center'));151152this.sessionTitleElement = append(center, $('button.mobile-session-title'));153this.sessionTitleElement.setAttribute('type', 'button');154this.sessionTitleElement.textContent = localize('mobileTopBar.newSession', "New Session");155this._register(addDisposableListener(this.sessionTitleElement, EventType.CLICK, () => this._onDidClickTitle.fire()));156157this.actionsContainer = append(center, $('div.mobile-top-bar-actions'));158159// Right slot — laid out left-to-right in DOM order. The new-session160// (+) button is appended LAST so it always sits at the right edge,161// even when the changes pill is visible.162163// Changes pill — shown when in a chat that has produced changes.164// Tap → opens a file picker; selecting a file invokes the165// `sessions.mobile.openDiffView` command for that file's diff.166const changesPill = append(this.element, $('button.mobile-top-bar-button.mobile-changes-pill', { type: 'button' })) as HTMLButtonElement;167changesPill.setAttribute('aria-label', localize('mobileTopBar.changes', "View changes"));168changesPill.style.display = 'none';169const changesIcon = append(changesPill, $('span.mobile-changes-pill-icon'));170changesIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.diffMultiple));171const changesAddedEl = append(changesPill, $('span.mobile-changes-pill-added'));172const changesRemovedEl = append(changesPill, $('span.mobile-changes-pill-removed'));173this._register(addDisposableListener(changesPill, EventType.CLICK, () => this.showChangesPicker()));174175// New session button (+) — shown when in a chat, hidden on welcome.176// Always rightmost when in a chat.177const newSessionButton = append(this.element, $('button.mobile-top-bar-button.mobile-new-session-button'));178newSessionButton.setAttribute('aria-label', localize('mobileTopBar.newSessionAria', "New session"));179const newSessionIcon = append(newSessionButton, $('span'));180newSessionIcon.classList.add(...ThemeIcon.asClassNameArray(Codicon.plus));181this._register(addDisposableListener(newSessionButton, EventType.CLICK, () => this._onDidClickNewSession.fire()));182183// Account indicator — shown on welcome/new session, hidden in a chat184this.accountButton = append(this.element, $('button.mobile-top-bar-button.mobile-account-indicator'));185this.accountButton.setAttribute('aria-label', localize('mobileTopBar.account', "Account"));186this.accountAvatarElement = append(this.accountButton, $('img.mobile-account-avatar', { alt: '', draggable: 'false' })) as HTMLImageElement;187this.accountAvatarElement.decoding = 'async';188this.accountAvatarElement.referrerPolicy = 'no-referrer';189this.accountIconElement = append(this.accountButton, $('span'));190this.accountBadgeElement = append(this.accountButton, $('span.mobile-account-badge'));191this._register(addDisposableListener(this.accountButton, EventType.CLICK, () => this.showAccountPanel()));192193// Track account state — listen to multiple sources to catch194// updates regardless of service initialization ordering.195this._register(this.defaultAccountService.onDidChangeDefaultAccount(() => this.refreshAccount()));196this._register(this.authenticationService.onDidChangeSessions(() => this.refreshAccount()));197this._register(this.chatEntitlementService.onDidChangeEntitlement(() => this.renderAccountState()));198this._register(this.chatEntitlementService.onDidChangeSentiment(() => this.renderAccountState()));199this._register(this.chatEntitlementService.onDidChangeQuotaExceeded(() => this.renderAccountState()));200this._register(this.chatEntitlementService.onDidChangeQuotaRemaining(() => this.renderAccountState()));201this.refreshAccount();202203// Keep the title in sync with the active session204this._register(autorun(reader => {205const session = this.sessionsManagementService.activeSession.read(reader);206const title = session?.title.read(reader);207this.sessionTitleElement.textContent = title || localize('mobileTopBar.newSession', "New Session");208}));209210// Keep the changes pill in sync with the active session's changes.211// Hidden when there are no changes (counts are zero and list is empty).212const isNewChatRef = { value: !!IsNewChatSessionContext.getValue(contextKeyService) };213const renderChangesPill = () => {214const changes = this.latestChanges;215let added = 0;216let removed = 0;217for (const c of changes) {218added += c.insertions;219removed += c.deletions;220}221const hasChanges = changes.length > 0 && (added > 0 || removed > 0);222// Hide on welcome / new-chat — no session changes to view there.223const visible = hasChanges && !isNewChatRef.value;224changesPill.style.display = visible ? '' : 'none';225if (visible) {226changesAddedEl.textContent = `+${added}`;227changesRemovedEl.textContent = `-${removed}`;228changesPill.title = localize('mobileTopBar.changesTooltip', "{0} files changed (+{1} -{2})", changes.length, added, removed);229}230};231this._register(autorun(reader => {232const session = this.sessionsManagementService.activeSession.read(reader);233this.latestChanges = session?.changes.read(reader) ?? [];234renderChangesPill();235}));236237// Mount the center toolbar (host filter widget on web welcome, etc.)238const toolbar = this._register(instantiationService.createInstance(MenuWorkbenchToolBar, this.actionsContainer, Menus.MobileTitleBarCenter, {239hiddenItemStrategy: HiddenItemStrategy.NoHide,240telemetrySource: 'mobileTitlebar.center',241toolbarOptions: { primaryGroup: () => true },242}));243244// Switch between title and toolbar based on whether a new (empty)245// chat session is active AND whether the toolbar has anything to246// show. The latter is important because on desktop/electron or247// when no agent hosts are configured the toolbar can be empty —248// in that case we keep the title visible.249const newChatKeySet = new Set([IsNewChatSessionContext.key]);250const updateCenterMode = () => {251const isNewChat = !!IsNewChatSessionContext.getValue(contextKeyService);252const hasActions = toolbar.getItemsLength() > 0;253this.element.classList.toggle('show-actions', isNewChat && hasActions);254255// Right slot: swap between [+] (in-chat) and [account] (welcome)256newSessionButton.style.display = isNewChat ? 'none' : '';257this.accountButton.style.display = isNewChat ? '' : 'none';258259// Changes pill follows the in-chat state — hidden on welcome.260isNewChatRef.value = isNewChat;261renderChangesPill();262};263updateCenterMode();264this._register(contextKeyService.onDidChangeContext(e => {265if (e.affectsSome(newChatKeySet)) {266updateCenterMode();267}268if (e.affectsSome(sidebarVisibleKeySet)) {269updateSidebarIcon();270}271}));272this._register(toolbar.onDidChangeMenuItems(() => updateCenterMode()));273}274275/**276* Explicitly set the title shown in the center slot. Called only when277* overriding the live session title (tests, placeholders). The live278* subscription will overwrite this on the next session change.279*/280setTitle(title: string): void {281this.sessionTitleElement.textContent = title;282}283284// --- Changes Pill --- //285286/**287* Open a quick pick listing the files changed in the active session.288* Selecting one invokes {@link MOBILE_OPEN_DIFF_VIEW_COMMAND_ID} with289* the corresponding {@link IFileDiffViewData}.290*/291private showChangesPicker(): void {292const changes = this.latestChanges;293if (!changes.length) {294return;295}296297type Item = IQuickPickItem & { readonly diff: IFileDiffViewData };298const items: Item[] = changes.map(change => {299// IChatSessionFileChange2 carries `uri` (and may also have `modifiedUri`);300// the legacy IChatSessionFileChange only has `modifiedUri`. The discriminator301// is the presence of `uri` (required on the v2 shape, absent on v1).302const v2 = (change as { uri?: URI }).uri;303const modifiedURI = v2 ? (change.modifiedUri ?? v2) : change.modifiedUri!;304// `originalURI` may legitimately be undefined for newly-added files;305// MobileDiffView treats that as an empty original (all-added diff).306// Do NOT fall back to modifiedURI here — that would self-diff and307// render "No changes in this file." for added files.308const originalURI = change.originalUri;309const added = change.insertions;310const removed = change.deletions;311return {312label: basename(modifiedURI),313description: `+${added} -${removed}`,314detail: modifiedURI.path,315diff: {316originalURI,317modifiedURI,318identical: added === 0 && removed === 0,319added,320removed,321},322};323});324325const picker = this.quickInputService.createQuickPick<Item>();326picker.title = localize('mobileTopBar.changesPickerTitle', "Session Changes");327picker.placeholder = localize('mobileTopBar.changesPickerPlaceholder', "Select a file to view its diff");328picker.matchOnDescription = true;329picker.items = items;330const store = new DisposableStore();331store.add(picker.onDidAccept(() => {332const selected = picker.selectedItems[0];333if (selected) {334this.commandService.executeCommand(MOBILE_OPEN_DIFF_VIEW_COMMAND_ID, selected.diff);335}336picker.hide();337}));338store.add(picker.onDidHide(() => {339store.dispose();340picker.dispose();341}));342picker.show();343}344345// --- Account Indicator --- //346347private async refreshAccount(): Promise<void> {348const requestId = ++this.accountRequestCounter;349this.isAccountLoading = true;350this.renderAccountState();351352const info = await resolveAccountInfo(this.defaultAccountService, this.authenticationService);353if (requestId !== this.accountRequestCounter) {354return;355}356357this.accountName = info?.accountName;358this.accountProviderId = info?.accountProviderId;359this.accountProviderLabel = info?.accountProviderLabel;360this.isAccountLoading = false;361this.refreshAvatar();362this.renderAccountState();363}364365private renderAccountState(): void {366// When we have a session from the auth service but the entitlement367// service hasn't resolved yet (still Unknown), treat it as the368// account being available rather than signed out. This avoids369// showing "Sign In" right after the walkthrough completes.370const entitlement = this.accountName && this.chatEntitlementService.entitlement === ChatEntitlement.Unknown371? ChatEntitlement.Unresolved372: this.chatEntitlementService.entitlement;373374const state = getAccountTitleBarState({375isAccountLoading: this.isAccountLoading,376accountName: this.accountName,377accountProviderLabel: this.accountProviderLabel,378entitlement,379sentiment: this.chatEntitlementService.sentiment,380quotas: this.chatEntitlementService.quotas,381});382383// Avatar384const hasAvatar = !!this.loadedAvatarUrl && !this.isAccountLoading;385this.accountAvatarElement.classList.toggle('visible', hasAvatar);386if (hasAvatar && this.accountAvatarElement.src !== this.loadedAvatarUrl) {387this.accountAvatarElement.src = this.loadedAvatarUrl!;388} else if (!hasAvatar) {389this.accountAvatarElement.removeAttribute('src');390}391392// Codicon fallback393const titleBarIcon = state.dotBadge ? Codicon.account : state.icon;394this.accountIconElement.className = ThemeIcon.asClassName(titleBarIcon);395this.accountIconElement.classList.toggle('hidden', hasAvatar);396397// Dot badge398const badgeKey = getAccountTitleBarBadgeKey(state);399if (badgeKey !== this.lastBadgeKey) {400this.lastBadgeKey = badgeKey;401this.dismissedBadgeKey = undefined;402}403const showBadge = !!badgeKey && badgeKey !== this.dismissedBadgeKey;404this.accountBadgeElement.style.display = showBadge ? '' : 'none';405this.accountBadgeElement.classList.toggle('dot-badge-warning', showBadge && state.dotBadge === 'warning');406this.accountBadgeElement.classList.toggle('dot-badge-error', showBadge && state.dotBadge === 'error');407408// ARIA409this.accountButton.setAttribute('aria-label', state.ariaLabel);410}411412private refreshAvatar(): void {413const avatarUrl = getAccountProfileImageUrl(this.accountProviderId, this.accountName);414if (avatarUrl === this.currentAvatarUrl) {415return;416}417418this.currentAvatarUrl = avatarUrl;419this.loadedAvatarUrl = undefined;420this.avatarLoadDisposable.clear();421const requestId = ++this.avatarRequestCounter;422423if (!avatarUrl) {424this.renderAccountState();425return;426}427428const image = new Image();429image.referrerPolicy = 'no-referrer';430const clearHandlers = () => { image.onload = null; image.onerror = null; };431image.onload = () => {432if (requestId !== this.avatarRequestCounter) { return; }433this.loadedAvatarUrl = avatarUrl;434this.renderAccountState();435clearHandlers();436};437image.onerror = () => {438if (requestId !== this.avatarRequestCounter) { return; }439this.loadedAvatarUrl = undefined;440this.renderAccountState();441clearHandlers();442};443this.avatarLoadDisposable.value = toDisposable(() => { clearHandlers(); image.src = ''; });444image.src = avatarUrl;445}446447// --- Account Sheet --- //448449private showAccountPanel(): void {450if (this.isAccountMenuVisible) {451this.accountPanelDisposable.clear();452return;453}454455this.accountPanelDisposable.clear();456457const panelStore = new DisposableStore();458this.accountPanelDisposable.value = panelStore;459460const badgeKey = getAccountTitleBarBadgeKey(getAccountTitleBarState({461isAccountLoading: this.isAccountLoading,462accountName: this.accountName,463accountProviderLabel: this.accountProviderLabel,464entitlement: this.chatEntitlementService.entitlement,465sentiment: this.chatEntitlementService.sentiment,466quotas: this.chatEntitlementService.quotas,467}));468if (badgeKey) {469this.dismissedBadgeKey = badgeKey;470}471472this.isAccountMenuVisible = true;473this.renderAccountState();474panelStore.add({475dispose: () => {476this.isAccountMenuVisible = false;477this.copilotDashboardStore.clear();478this.renderAccountState();479}480});481482const closeSheet = () => this.accountPanelDisposable.clear();483484// Full-screen sheet inside the workbench container485const workbenchContainer = this.element.parentElement!;486const sheet = append(workbenchContainer, $('div.mobile-account-sheet'));487panelStore.add(toDisposable(() => sheet.remove()));488489// Header: title + close button490const header = append(sheet, $('div.mobile-account-sheet-header'));491const headerTitle = append(header, $('h2.mobile-account-sheet-title'));492headerTitle.textContent = localize('mobileAccount.title', "Account");493const closeButton = append(header, $('button.mobile-account-sheet-close', { type: 'button' })) as HTMLButtonElement;494closeButton.setAttribute('aria-label', localize('mobileAccount.close', "Close"));495append(closeButton, $('span')).classList.add(...ThemeIcon.asClassNameArray(Codicon.close));496panelStore.add(addDisposableListener(closeButton, EventType.CLICK, closeSheet));497498// Scrollable content499const content = append(sheet, $('div.mobile-account-sheet-content'));500501// Profile section502const profile = append(content, $('div.mobile-account-sheet-profile'));503if (this.loadedAvatarUrl) {504const avatar = append(profile, $('img.mobile-account-sheet-avatar', { alt: '', draggable: 'false' })) as HTMLImageElement;505avatar.src = this.loadedAvatarUrl;506avatar.referrerPolicy = 'no-referrer';507avatar.decoding = 'async';508} else {509const avatarPlaceholder = append(profile, $('div.mobile-account-sheet-avatar-placeholder'));510append(avatarPlaceholder, $('span')).classList.add(...ThemeIcon.asClassNameArray(Codicon.account));511}512const profileInfo = append(profile, $('div.mobile-account-sheet-profile-info'));513if (this.isAccountLoading) {514append(profileInfo, $('div.mobile-account-sheet-name')).textContent = localize('mobileAccount.loading', "Loading...");515} else if (this.accountName) {516append(profileInfo, $('div.mobile-account-sheet-name')).textContent = this.accountName;517if (this.accountProviderLabel) {518append(profileInfo, $('div.mobile-account-sheet-provider')).textContent = this.accountProviderLabel;519}520} else {521append(profileInfo, $('div.mobile-account-sheet-name')).textContent = localize('mobileAccount.signedOut', "Not signed in");522}523524// Copilot status dashboard — only when signed in AND entitlements525// have resolved. When entitlement is Unknown or Available (setup526// pending), the dashboard shows a "Set up Copilot" prompt that527// doesn't apply in the agents app.528const entitlement = this.chatEntitlementService.entitlement;529const showDashboard = !this.chatEntitlementService.sentiment.hidden530&& !!this.accountName531&& entitlement !== ChatEntitlement.Unknown532&& entitlement !== ChatEntitlement.Available;533if (showDashboard) {534const dashboardSection = append(content, $('div.mobile-account-sheet-section'));535const store = new DisposableStore();536this.copilotDashboardStore.value = store;537const dashboardElement = this.chatDashboardService.createDashboardElement(store);538if (dashboardElement) {539append(dashboardSection, dashboardElement);540}541}542543// Actions list544const actionsSection = append(content, $('div.mobile-account-sheet-actions'));545const allActions = this.getSheetActions();546for (const action of allActions) {547if (action instanceof Separator) {548append(actionsSection, $('div.mobile-account-sheet-separator'));549continue;550}551const row = append(actionsSection, $('button.mobile-account-sheet-action', { type: 'button' })) as HTMLButtonElement;552row.disabled = !action.enabled;553row.setAttribute('aria-label', action.tooltip || action.label);554const icon = this.getActionIcon(action);555if (icon) {556append(row, $('span.mobile-account-sheet-action-icon')).classList.add(...ThemeIcon.asClassNameArray(icon));557}558append(row, $('span.mobile-account-sheet-action-label')).textContent = action.label;559panelStore.add(addDisposableListener(row, EventType.CLICK, async event => {560event.preventDefault();561event.stopPropagation();562closeSheet();563await Promise.resolve(action.run());564}));565}566}567568private getSheetActions(): IAction[] {569const menu = this.menuService.createMenu(Menus.AccountMenu, this.contextKeyService);570const rawActions: IAction[] = [];571fillInActionBarActions(menu.getActions(), rawActions);572menu.dispose();573return rawActions.filter(action => {574if (action instanceof Separator) {575return true;576}577if (this.isAccountLoading && action.id === 'workbench.action.agenticSignIn') {578return false;579}580return !action.id.startsWith('update.');581});582}583584private getActionIcon(action: IAction): ThemeIcon | undefined {585switch (action.id) {586case 'workbench.action.openSettings': return Codicon.settingsGear;587case 'workbench.action.agenticSignOut': return Codicon.signOut;588case 'workbench.action.agenticSignIn': return Codicon.signIn;589default: return undefined;590}591}592}593594595