Path: blob/main/src/vs/sessions/contrib/welcome/browser/welcome.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 { isWeb } from '../../../../base/common/platform.js';6import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';8import { localize2 } from '../../../../nls.js';9import { ILogService } from '../../../../platform/log/common/log.js';10import { IProductService } from '../../../../platform/product/common/productService.js';11import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';12import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';13import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';14import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';15import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';16import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';17import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';18import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';19import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js';20import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';21import { SessionsWalkthroughOverlay, WalkthroughOutcome } from './sessionsWalkthrough.js';22import { WELCOME_COMPLETE_KEY } from '../../../common/welcome.js';2324function shouldSkipSessionsWelcome(environmentService: IWorkbenchEnvironmentService): boolean {25const envArgs = (environmentService as IWorkbenchEnvironmentService & { args?: Record<string, unknown> }).args;26if (envArgs?.['skip-sessions-welcome']) {27return true;28}2930return typeof globalThis.location !== 'undefined' && new URLSearchParams(globalThis.location.search).has('skip-sessions-welcome');31}3233function shouldPersistWelcomeCompletion(outcome: WalkthroughOutcome, defaultAccountService: IDefaultAccountService): boolean {34return outcome === 'completed' || defaultAccountService.currentDefaultAccount !== null;35}3637export function resetSessionsWelcome(38storageService: Pick<IStorageService, 'remove' | 'store'>,39instantiationService: IInstantiationService,40layoutService: IWorkbenchLayoutService,41defaultAccountService: IDefaultAccountService,42contextKeyService: IContextKeyService,43environmentService: IWorkbenchEnvironmentService,44logService: ILogService,45): void {46// Clear completion marker47storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);4849if (shouldSkipSessionsWelcome(environmentService)) {50return;51}5253// Immediately show the walkthrough overlay54const store = new DisposableStore();55const welcomeVisibleKey = SessionsWelcomeVisibleContext.bindTo(contextKeyService);56welcomeVisibleKey.set(true);57store.add(toDisposable(() => welcomeVisibleKey.reset()));5859const walkthrough = store.add(instantiationService.createInstance(60SessionsWalkthroughOverlay,61layoutService.mainContainer,62true,63));6465store.add(defaultAccountService.onDidChangeDefaultAccount(account => {66if (!walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) {67storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);68walkthrough.showThemeStep();69}70}));7172walkthrough.outcome73.then(outcome => {74logService.info(`[sessions welcome] Developer reset walkthrough finished with outcome: ${outcome}`);75if (shouldPersistWelcomeCompletion(outcome, defaultAccountService)) {76storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);77}78})79.finally(() => {80store.dispose();81});82}8384export class SessionsWelcomeContribution extends Disposable implements IWorkbenchContribution {8586static readonly ID = 'workbench.contrib.sessionsWelcome';8788private readonly overlayRef = this._register(new MutableDisposable<DisposableStore>());89private readonly watcherRef = this._register(new MutableDisposable());9091constructor(92@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,93@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,94@IInstantiationService private readonly instantiationService: IInstantiationService,95@IProductService private readonly productService: IProductService,96@IStorageService private readonly storageService: IStorageService,97@IContextKeyService private readonly contextKeyService: IContextKeyService,98@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,99@IAuthenticationService private readonly authenticationService: IAuthenticationService,100@ILogService private readonly logService: ILogService,101) {102super();103104if (!this.productService.defaultChatAgent?.chatExtensionId) {105return;106}107108// Allow automated tests to skip the welcome overlay entirely.109// Desktop: --skip-sessions-welcome CLI flag110// Web: ?skip-sessions-welcome query parameter111if (shouldSkipSessionsWelcome(this.environmentService)) {112return;113}114115if (isWeb) {116// On web, show the walkthrough if the user is not authenticated.117// Auth is handled by the walkthrough's GitHub button via118// IAuthenticationService. Discovery runs separately after auth.119this._checkWebAuth();120this._watchWebAuth();121return;122}123const isFirstLaunch = !this.storageService.getBoolean(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION, false);124125if (isFirstLaunch) {126// First launch: show the overlay immediately with a loading animation127// while the default account resolves, then render the appropriate screen.128this.showWalkthrough(true);129} else {130// Returning user: don't block with a loading screen — resolve the account131// in the background. If signed out, showWalkthrough will be called then.132this.watchSignInState();133}134}135136/**137* Web-only: check if the user has a GitHub session. If not, show the138* walkthrough so they can sign in. If they're already authenticated,139* skip the walkthrough and let discovery handle the rest.140*/141private async _checkWebAuth(): Promise<void> {142try {143const sessions = await this.authenticationService.getSessions('github');144if (sessions.length > 0) {145this.logService.info('[sessions welcome] GitHub session found on web, skipping walkthrough');146this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);147return;148}149} catch {150// Provider not available yet — show walkthrough151}152this.showWalkthrough(false);153}154155/**156* Web-only: react to GitHub session loss. When the user's last GitHub157* session is removed (token expired, secret storage wiped, or explicit158* sign-out from the account menu), clear the welcome completion marker159* and show the sign-in walkthrough again. Without this, passive sign-out160* leaves the user on a seemingly-working workbench with a stale UI.161*162* Also watches for passive token expiry on web.163*/164private _watchWebAuth(): void {165this._register(this.authenticationService.onDidChangeSessions(async e => {166if (e.providerId !== 'github' || !e.event.removed?.length) {167return;168}169try {170const remaining = await this.authenticationService.getSessions('github');171if (remaining.length > 0) {172return;173}174} catch {175// Provider became unavailable — treat as signed out176}177this.logService.info('[sessions welcome] GitHub session removed on web, re-showing walkthrough');178this.storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);179this.showWalkthrough(false);180}));181}182183/**184* Watches the default account after setup has already completed. If the185* user signs out, shows the welcome (sign-in) overlay again. Also186* handles the case where the account resolves to null at startup (the187* user was signed out since their last session).188*/189private async watchSignInState(): Promise<void> {190const initialAccount = await this.defaultAccountService.getDefaultAccount();191if (this.overlayRef.value) {192return; // overlay already shown by another path193}194if (!initialAccount) {195this.showWalkthrough(false);196return;197}198let signedIn = true;199this.watcherRef.value = this.defaultAccountService.onDidChangeDefaultAccount(account => {200const nowSignedIn = account !== null;201if (signedIn && !nowSignedIn) {202// Clear the completion marker so that on the next reload the203// welcome overlay's loading animation covers startup, instead204// of briefly showing the workbench before the sign-in screen.205this.storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);206this.showWalkthrough(false);207}208signedIn = nowSignedIn;209});210}211212private showWalkthrough(isFirstLaunch: boolean): void {213if (this.overlayRef.value) {214return;215}216217this.watcherRef.clear();218this.overlayRef.value = new DisposableStore();219let welcomeCompletionStored = false;220221// Mark the welcome overlay as visible for titlebar disabling222const welcomeVisibleKey = SessionsWelcomeVisibleContext.bindTo(this.contextKeyService);223welcomeVisibleKey.set(true);224this.overlayRef.value.add(toDisposable(() => welcomeVisibleKey.reset()));225226const walkthrough = this.overlayRef.value.add(this.instantiationService.createInstance(227SessionsWalkthroughOverlay,228this.layoutService.mainContainer,229isFirstLaunch,230));231232// When the user signs in, persist completion and finish the walkthrough.233// Only auto-complete once the sign-in screen is actually visible — not234// during the loading phase — so external account resolution (e.g. VS Code235// signing in while the Agents loading animation is still showing) cannot236// dismiss the overlay before the user has seen or interacted with it.237this.overlayRef.value.add(this.defaultAccountService.onDidChangeDefaultAccount(account => {238if (!welcomeCompletionStored && !walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) {239welcomeCompletionStored = true;240this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);241walkthrough.showThemeStep();242}243}));244245// Handle the walkthrough outcome246walkthrough.outcome.then(outcome => {247this.logService.info(`[sessions welcome] Walkthrough finished with outcome: ${outcome}`);248if (this._store.isDisposed) {249return;250}251if (!welcomeCompletionStored && shouldPersistWelcomeCompletion(outcome, this.defaultAccountService)) {252welcomeCompletionStored = true;253this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);254}255this.overlayRef.clear();256this.watchSignInState();257});258}259}260261registerWorkbenchContribution2(SessionsWelcomeContribution.ID, SessionsWelcomeContribution, WorkbenchPhase.BlockRestore);262263registerAction2(class extends Action2 {264constructor() {265super({266id: 'workbench.action.resetSessionsWelcome',267title: localize2('resetSessionsWelcome', "Reset Agents Welcome"),268category: Categories.Developer,269f1: true,270});271}272run(accessor: ServicesAccessor): void {273const storageService = accessor.get(IStorageService);274const instantiationService = accessor.get(IInstantiationService);275const layoutService = accessor.get(IWorkbenchLayoutService);276const defaultAccountService = accessor.get(IDefaultAccountService);277const contextKeyService = accessor.get(IContextKeyService);278const environmentService = accessor.get(IWorkbenchEnvironmentService);279const logService = accessor.get(ILogService);280resetSessionsWelcome(storageService, instantiationService, layoutService, defaultAccountService, contextKeyService, environmentService, logService);281}282});283284285