This commit is contained in:
Osvaldo Ortega
2026-04-27 08:23:37 -07:00
parent e2edcdcb0f
commit c411ee3862
8 changed files with 108 additions and 45 deletions

View File

@@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { localize } from '../../../../nls.js';
import { ChatEntitlement, IChatSentiment, IQuotaSnapshot } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
import { Codicon } from '../../base/common/codicons.js';
import { ThemeIcon } from '../../base/common/themables.js';
import { localize } from '../../nls.js';
import { ChatEntitlement, IQuotaSnapshot } from '../../workbench/services/chat/common/chatEntitlementService.js';
export type AccountTitleBarStateSource = 'account' | 'copilot';
export type AccountTitleBarStateKind = 'default' | 'accent' | 'warning' | 'prominent';
@@ -16,7 +16,7 @@ export interface IAccountTitleBarStateContext {
readonly accountName?: string;
readonly accountProviderLabel?: string;
readonly entitlement: ChatEntitlement;
readonly sentiment: Pick<IChatSentiment, 'hidden' | 'disabled' | 'untrusted'>;
readonly sentiment: { readonly hidden?: boolean; readonly disabled?: boolean; readonly untrusted?: boolean };
readonly quotas: {
readonly chat?: IQuotaSnapshot;
readonly completions?: IQuotaSnapshot;
@@ -91,7 +91,7 @@ export function getAccountTitleBarState(context: IAccountTitleBarStateContext):
function getCopilotPresentation(
entitlement: ChatEntitlement,
sentiment: Pick<IChatSentiment, 'hidden' | 'disabled' | 'untrusted'>,
sentiment: { readonly hidden?: boolean; readonly disabled?: boolean; readonly untrusted?: boolean },
quotas: { readonly chat?: IQuotaSnapshot; readonly completions?: IQuotaSnapshot }
): IAccountTitleBarState | undefined {
if (sentiment.hidden) {

View File

@@ -0,0 +1,27 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from '../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../platform/instantiation/common/instantiation.js';
export const IChatDashboardService = createDecorator<IChatDashboardService>('chatDashboardService');
export interface IChatDashboardService {
readonly _serviceBrand: undefined;
/**
* Creates a chat status dashboard element embedded in a container div.
* Returns `undefined` if the dashboard is not available.
*/
createDashboardElement(store: DisposableStore): HTMLElement | undefined;
}
class NullChatDashboardService implements IChatDashboardService {
readonly _serviceBrand: undefined;
createDashboardElement(): HTMLElement | undefined { return undefined; }
}
registerSingleton(IChatDashboardService, NullChatDashboardService, InstantiationType.Delayed);

View File

@@ -5,8 +5,7 @@
import './mobileChatShell.css';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { $, addDisposableListener, append, disposableWindowInterval, EventType } from '../../../../base/browser/dom.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { $, addDisposableListener, append, EventType } from '../../../../base/browser/dom.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { Codicon } from '../../../../base/common/codicons.js';
@@ -25,8 +24,8 @@ import { IsNewChatSessionContext } from '../../../common/contextkeys.js';
import { SideBarVisibleContext } from '../../../../workbench/common/contextkeys.js';
import { Menus } from '../../menus.js';
import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
import { getAccountTitleBarState, getAccountProfileImageUrl, getAccountTitleBarBadgeKey } from '../../../contrib/accountMenu/browser/accountTitleBarState.js';
import { ChatStatusDashboard } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.js';
import { getAccountTitleBarState, getAccountProfileImageUrl, getAccountTitleBarBadgeKey } from '../../accountTitleBarState.js';
import { IChatDashboardService } from '../../chatDashboardService.js';
/**
* Mobile titlebar — prepended above the workbench grid on phone viewports
@@ -34,8 +33,8 @@ import { ChatStatusDashboard } from '../../../../workbench/contrib/chat/browser/
*
* Layout (contextual right slot):
*
* - **In a chat session** → `[] [session title] [+]`
* - **Welcome / new session** → `[] [host widget | title] [account]`
* - **In a chat session** → `[toggle sidebar] [session title] [+]`
* - **Welcome / new session** → `[toggle sidebar] [host widget | title] [account]`
*
* The center slot switches content based on whether the sessions welcome
* (home/empty) screen is visible:
@@ -94,13 +93,14 @@ export class MobileTitlebarPart extends Disposable {
constructor(
parent: HTMLElement,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IInstantiationService instantiationService: IInstantiationService,
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
@IMenuService private readonly menuService: IMenuService,
@IChatDashboardService private readonly chatDashboardService: IChatDashboardService,
) {
super();
@@ -382,6 +382,7 @@ export class MobileTitlebarPart extends Disposable {
panelStore.add({
dispose: () => {
this.isAccountMenuVisible = false;
this.copilotDashboardStore.clear();
this.renderAccountState();
}
});
@@ -441,18 +442,10 @@ export class MobileTitlebarPart extends Disposable {
const dashboardSection = append(content, $('div.mobile-account-sheet-section'));
const store = new DisposableStore();
this.copilotDashboardStore.value = store;
const dashboardElement = ChatStatusDashboard.instantiateInContents(this.instantiationService, store, {
disableInlineSuggestionsSettings: true,
disableModelSelection: true,
disableProviderOptions: true,
disableCompletionsSnooze: true,
});
store.add(disposableWindowInterval(mainWindow, () => {
if (!dashboardElement.isConnected) {
store.dispose();
}
}, 2000));
append(dashboardSection, dashboardElement);
const dashboardElement = this.chatDashboardService.createDashboardElement(store);
if (dashboardElement) {
append(dashboardSection, dashboardElement);
}
}
// Actions list

View File

@@ -34,16 +34,18 @@ import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { URI } from '../../../../base/common/uri.js';
import { isWindows, isMacintosh } from '../../../../base/common/platform.js';
import { UpdateHoverWidget } from './updateHoverWidget.js';
import { ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
import { ChatStatusDashboard } from '../../../../workbench/contrib/chat/browser/chatStatus/chatStatusDashboard.js';
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { getAccountProfileImageUrl, getAccountTitleBarBadgeKey, getAccountTitleBarState } from './accountTitleBarState.js';
import { getAccountProfileImageUrl, getAccountTitleBarBadgeKey, getAccountTitleBarState } from '../../../browser/accountTitleBarState.js';
import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
import { IsAuxiliaryWindowContext } from '../../../../workbench/common/contextkeys.js';
import { IAuthenticationAccessService } from '../../../../workbench/services/authentication/browser/authenticationAccessService.js';
import { IAuthenticationUsageService } from '../../../../workbench/services/authentication/browser/authenticationUsageService.js';
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
import { IChatDashboardService } from '../../../browser/chatDashboardService.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
// --- Account Menu Items --- //
const AccountMenu = Menus.AccountMenu;
@@ -827,3 +829,32 @@ class AccountWidgetContribution extends Disposable implements IWorkbenchContribu
}
registerWorkbenchContribution2(AccountWidgetContribution.ID, AccountWidgetContribution, WorkbenchPhase.BlockRestore);
// --- Chat Dashboard Service (real implementation for mobile account sheet) --- //
class ChatDashboardServiceImpl implements IChatDashboardService {
readonly _serviceBrand: undefined;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
) { }
createDashboardElement(store: DisposableStore): HTMLElement | undefined {
const dashboardElement = ChatStatusDashboard.instantiateInContents(this.instantiationService, store, {
disableInlineSuggestionsSettings: true,
disableModelSelection: true,
disableProviderOptions: true,
disableCompletionsSnooze: true,
});
store.add(disposableWindowInterval(mainWindow, () => {
if (!dashboardElement.isConnected) {
store.dispose();
}
}, 2000));
return dashboardElement;
}
}
registerSingleton(IChatDashboardService, ChatDashboardServiceImpl, InstantiationType.Delayed);

View File

@@ -6,7 +6,7 @@
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ChatEntitlement } from '../../../../../workbench/services/chat/common/chatEntitlementService.js';
import { getAccountProfileImageUrl, getAccountTitleBarBadgeKey, getAccountTitleBarState, IAccountTitleBarStateContext } from '../../browser/accountTitleBarState.js';
import { getAccountProfileImageUrl, getAccountTitleBarBadgeKey, getAccountTitleBarState, IAccountTitleBarStateContext } from '../../../../browser/accountTitleBarState.js';
suite('Sessions - Account Title Bar State', () => {

View File

@@ -13,7 +13,6 @@ import { ILogService } from '../../../../platform/log/common/log.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { isWeb } from '../../../../base/common/platform.js';
import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
import { URI } from '../../../../base/common/uri.js';
@@ -76,7 +75,6 @@ export class SessionsWalkthroughOverlay extends Disposable {
constructor(
container: HTMLElement,
private readonly _isFirstLaunch: boolean,
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@ICommandService private readonly commandService: ICommandService,

View File

@@ -5,6 +5,7 @@
import { isWeb } from '../../../../base/common/platform.js';
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { autorun } from '../../../../base/common/observable.js';
import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
import { localize2 } from '../../../../nls.js';
import { ILogService } from '../../../../platform/log/common/log.js';
@@ -19,6 +20,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js';
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
import { ChatEntitlement, ChatEntitlementService, IChatEntitlementService } from '../../../../workbench/services/chat/common/chatEntitlementService.js';
import { SessionsWalkthroughOverlay, WalkthroughOutcome } from './sessionsWalkthrough.js';
import { WELCOME_COMPLETE_KEY } from '../../../common/welcome.js';
@@ -99,6 +101,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
@ILogService private readonly logService: ILogService,
) {
super();
@@ -188,10 +191,23 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
// the welcome completion marker is still set (session exists but is
// unusable). Only triggers after the user has previously completed
// sign-in — avoids firing during initial load.
//
// The first autorun evaluation is skipped to avoid a false positive:
// if entitlement starts as non-Unknown (e.g. Unresolved from cache)
// and then transitions to Unknown on the first network check, we
// don't want to re-show the walkthrough during normal startup.
let isFirstRun = true;
let wasSignedIn = false;
this._register(autorun(reader => {
this.chatEntitlementService.entitlementObs.read(reader);
const entitlement = this.chatEntitlementService.entitlement;
if (isFirstRun) {
isFirstRun = false;
if (entitlement !== ChatEntitlement.Unknown) {
wasSignedIn = true;
}
return;
}
if (entitlement !== ChatEntitlement.Unknown) {
wasSignedIn = true;
return;
@@ -205,7 +221,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
}
this.logService.info('[sessions welcome] Entitlement became Unknown on web (token expired), re-showing walkthrough');
this.storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);
this.showWalkthrough();
this.showWalkthrough(false);
}));
}

View File

@@ -334,9 +334,9 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
this.logService.error('[DefaultAccount] Error while waiting for installed extensions to be registered', getErrorMessage(error));
}
console.log('[DefaultAccount] Starting initialization');
this.logService.debug('[DefaultAccount] Starting initialization');
await this.doUpdateDefaultAccount();
console.log('[DefaultAccount] Initialization complete. Account:', this._defaultAccount?.defaultAccount.accountName ?? 'null');
this.logService.debug('[DefaultAccount] Initialization complete');
this._register(this.onDidChangeDefaultAccount(account => {
this.telemetryService.publicLog2<DefaultAccountStatusTelemetry, DefaultAccountStatusTelemetryClassification>('defaultaccount:status', { status: account ? 'available' : 'unavailable', initial: false });
@@ -434,7 +434,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
const declaredProvider = this.authenticationService.declaredProviders.find(provider => provider.id === defaultAccountProvider.id);
if (!declaredProvider) {
console.log(`[DefaultAccount] Authentication provider is not declared.`, defaultAccountProvider);
this.logService.info(`[DefaultAccount] Authentication provider is not declared.`, defaultAccountProvider);
return null;
}
@@ -517,15 +517,13 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider, options?: { forceRefresh?: boolean }): Promise<IDefaultAccountData | null> {
try {
console.log('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id);
this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id);
const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes);
if (!sessions?.length) {
console.log('[DefaultAccount] No matching session found for provider:', authenticationProvider.id, 'Expected scopes:', JSON.stringify(this.defaultAccountConfig.authenticationProvider.scopes));
this.logService.debug('[DefaultAccount] No matching session found for provider:', authenticationProvider.id);
return null;
}
console.log('[DefaultAccount] Found', sessions.length, 'matching session(s). Account:', sessions[0].account.label, 'Scopes:', sessions[0].scopes);
return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions, options);
} catch (error) {
this.logService.error('[DefaultAccount] Failed to get default account for provider:', authenticationProvider.id, getErrorMessage(error));
@@ -588,25 +586,25 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
private async findMatchingProviderSession(authProviderId: string, allScopes: string[][]): Promise<AuthenticationSession[] | undefined> {
const sessions = await this.getSessions(authProviderId);
console.log('[DefaultAccount] Got', sessions.length, 'total session(s) for provider:', authProviderId);
// When no scopes are configured (e.g. vscode.dev where product
// config may not include providerScopes), accept any session.
if (allScopes.length === 0 && sessions.length > 0) {
console.log('[DefaultAccount] No scopes configured, accepting all sessions');
// When no scopes are configured on web (e.g. vscode.dev where
// product config may not include providerScopes), accept any
// session. This only applies to web — on desktop, scopes should
// always be configured via product.json.
if (isWeb && allScopes.length === 0 && sessions.length > 0) {
this.logService.debug('[DefaultAccount] No scopes configured on web, accepting all sessions');
return [...sessions];
}
const matchingSessions = [];
for (const session of sessions) {
console.log('[DefaultAccount] Checking session', session.id, 'account:', session.account.label, 'scopes:', session.scopes);
this.logService.debug('[DefaultAccount] Checking session with scopes', session.scopes);
for (const scopes of allScopes) {
if (this.scopesMatch(session.scopes, scopes)) {
matchingSessions.push(session);
}
}
}
console.log('[DefaultAccount] Matching sessions:', matchingSessions.length);
return matchingSessions.length > 0 ? matchingSessions : undefined;
}