Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { isWeb } from '../../../../base/common/platform.js';
7
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
9
import { localize2 } from '../../../../nls.js';
10
import { ILogService } from '../../../../platform/log/common/log.js';
11
import { IProductService } from '../../../../platform/product/common/productService.js';
12
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
13
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
14
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
15
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
16
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
17
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
18
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
19
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
20
import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js';
21
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
22
import { SessionsWalkthroughOverlay, WalkthroughOutcome } from './sessionsWalkthrough.js';
23
import { WELCOME_COMPLETE_KEY } from '../../../common/welcome.js';
24
25
function shouldSkipSessionsWelcome(environmentService: IWorkbenchEnvironmentService): boolean {
26
const envArgs = (environmentService as IWorkbenchEnvironmentService & { args?: Record<string, unknown> }).args;
27
if (envArgs?.['skip-sessions-welcome']) {
28
return true;
29
}
30
31
return typeof globalThis.location !== 'undefined' && new URLSearchParams(globalThis.location.search).has('skip-sessions-welcome');
32
}
33
34
function shouldPersistWelcomeCompletion(outcome: WalkthroughOutcome, defaultAccountService: IDefaultAccountService): boolean {
35
return outcome === 'completed' || defaultAccountService.currentDefaultAccount !== null;
36
}
37
38
export function resetSessionsWelcome(
39
storageService: Pick<IStorageService, 'remove' | 'store'>,
40
instantiationService: IInstantiationService,
41
layoutService: IWorkbenchLayoutService,
42
defaultAccountService: IDefaultAccountService,
43
contextKeyService: IContextKeyService,
44
environmentService: IWorkbenchEnvironmentService,
45
logService: ILogService,
46
): void {
47
// Clear completion marker
48
storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);
49
50
if (shouldSkipSessionsWelcome(environmentService)) {
51
return;
52
}
53
54
// Immediately show the walkthrough overlay
55
const store = new DisposableStore();
56
const welcomeVisibleKey = SessionsWelcomeVisibleContext.bindTo(contextKeyService);
57
welcomeVisibleKey.set(true);
58
store.add(toDisposable(() => welcomeVisibleKey.reset()));
59
60
const walkthrough = store.add(instantiationService.createInstance(
61
SessionsWalkthroughOverlay,
62
layoutService.mainContainer,
63
true,
64
));
65
66
store.add(defaultAccountService.onDidChangeDefaultAccount(account => {
67
if (!walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) {
68
storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
69
walkthrough.showThemeStep();
70
}
71
}));
72
73
walkthrough.outcome
74
.then(outcome => {
75
logService.info(`[sessions welcome] Developer reset walkthrough finished with outcome: ${outcome}`);
76
if (shouldPersistWelcomeCompletion(outcome, defaultAccountService)) {
77
storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
78
}
79
})
80
.finally(() => {
81
store.dispose();
82
});
83
}
84
85
export class SessionsWelcomeContribution extends Disposable implements IWorkbenchContribution {
86
87
static readonly ID = 'workbench.contrib.sessionsWelcome';
88
89
private readonly overlayRef = this._register(new MutableDisposable<DisposableStore>());
90
private readonly watcherRef = this._register(new MutableDisposable());
91
92
constructor(
93
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
94
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
95
@IInstantiationService private readonly instantiationService: IInstantiationService,
96
@IProductService private readonly productService: IProductService,
97
@IStorageService private readonly storageService: IStorageService,
98
@IContextKeyService private readonly contextKeyService: IContextKeyService,
99
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
100
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
101
@ILogService private readonly logService: ILogService,
102
) {
103
super();
104
105
if (!this.productService.defaultChatAgent?.chatExtensionId) {
106
return;
107
}
108
109
// Allow automated tests to skip the welcome overlay entirely.
110
// Desktop: --skip-sessions-welcome CLI flag
111
// Web: ?skip-sessions-welcome query parameter
112
if (shouldSkipSessionsWelcome(this.environmentService)) {
113
return;
114
}
115
116
if (isWeb) {
117
// On web, show the walkthrough if the user is not authenticated.
118
// Auth is handled by the walkthrough's GitHub button via
119
// IAuthenticationService. Discovery runs separately after auth.
120
this._checkWebAuth();
121
this._watchWebAuth();
122
return;
123
}
124
const isFirstLaunch = !this.storageService.getBoolean(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION, false);
125
126
if (isFirstLaunch) {
127
// First launch: show the overlay immediately with a loading animation
128
// while the default account resolves, then render the appropriate screen.
129
this.showWalkthrough(true);
130
} else {
131
// Returning user: don't block with a loading screen — resolve the account
132
// in the background. If signed out, showWalkthrough will be called then.
133
this.watchSignInState();
134
}
135
}
136
137
/**
138
* Web-only: check if the user has a GitHub session. If not, show the
139
* walkthrough so they can sign in. If they're already authenticated,
140
* skip the walkthrough and let discovery handle the rest.
141
*/
142
private async _checkWebAuth(): Promise<void> {
143
try {
144
const sessions = await this.authenticationService.getSessions('github');
145
if (sessions.length > 0) {
146
this.logService.info('[sessions welcome] GitHub session found on web, skipping walkthrough');
147
this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
148
return;
149
}
150
} catch {
151
// Provider not available yet — show walkthrough
152
}
153
this.showWalkthrough(false);
154
}
155
156
/**
157
* Web-only: react to GitHub session loss. When the user's last GitHub
158
* session is removed (token expired, secret storage wiped, or explicit
159
* sign-out from the account menu), clear the welcome completion marker
160
* and show the sign-in walkthrough again. Without this, passive sign-out
161
* leaves the user on a seemingly-working workbench with a stale UI.
162
*
163
* Also watches for passive token expiry on web.
164
*/
165
private _watchWebAuth(): void {
166
this._register(this.authenticationService.onDidChangeSessions(async e => {
167
if (e.providerId !== 'github' || !e.event.removed?.length) {
168
return;
169
}
170
try {
171
const remaining = await this.authenticationService.getSessions('github');
172
if (remaining.length > 0) {
173
return;
174
}
175
} catch {
176
// Provider became unavailable — treat as signed out
177
}
178
this.logService.info('[sessions welcome] GitHub session removed on web, re-showing walkthrough');
179
this.storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);
180
this.showWalkthrough(false);
181
}));
182
}
183
184
/**
185
* Watches the default account after setup has already completed. If the
186
* user signs out, shows the welcome (sign-in) overlay again. Also
187
* handles the case where the account resolves to null at startup (the
188
* user was signed out since their last session).
189
*/
190
private async watchSignInState(): Promise<void> {
191
const initialAccount = await this.defaultAccountService.getDefaultAccount();
192
if (this.overlayRef.value) {
193
return; // overlay already shown by another path
194
}
195
if (!initialAccount) {
196
this.showWalkthrough(false);
197
return;
198
}
199
let signedIn = true;
200
this.watcherRef.value = this.defaultAccountService.onDidChangeDefaultAccount(account => {
201
const nowSignedIn = account !== null;
202
if (signedIn && !nowSignedIn) {
203
// Clear the completion marker so that on the next reload the
204
// welcome overlay's loading animation covers startup, instead
205
// of briefly showing the workbench before the sign-in screen.
206
this.storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);
207
this.showWalkthrough(false);
208
}
209
signedIn = nowSignedIn;
210
});
211
}
212
213
private showWalkthrough(isFirstLaunch: boolean): void {
214
if (this.overlayRef.value) {
215
return;
216
}
217
218
this.watcherRef.clear();
219
this.overlayRef.value = new DisposableStore();
220
let welcomeCompletionStored = false;
221
222
// Mark the welcome overlay as visible for titlebar disabling
223
const welcomeVisibleKey = SessionsWelcomeVisibleContext.bindTo(this.contextKeyService);
224
welcomeVisibleKey.set(true);
225
this.overlayRef.value.add(toDisposable(() => welcomeVisibleKey.reset()));
226
227
const walkthrough = this.overlayRef.value.add(this.instantiationService.createInstance(
228
SessionsWalkthroughOverlay,
229
this.layoutService.mainContainer,
230
isFirstLaunch,
231
));
232
233
// When the user signs in, persist completion and finish the walkthrough.
234
// Only auto-complete once the sign-in screen is actually visible — not
235
// during the loading phase — so external account resolution (e.g. VS Code
236
// signing in while the Agents loading animation is still showing) cannot
237
// dismiss the overlay before the user has seen or interacted with it.
238
this.overlayRef.value.add(this.defaultAccountService.onDidChangeDefaultAccount(account => {
239
if (!welcomeCompletionStored && !walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) {
240
welcomeCompletionStored = true;
241
this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
242
walkthrough.showThemeStep();
243
}
244
}));
245
246
// Handle the walkthrough outcome
247
walkthrough.outcome.then(outcome => {
248
this.logService.info(`[sessions welcome] Walkthrough finished with outcome: ${outcome}`);
249
if (this._store.isDisposed) {
250
return;
251
}
252
if (!welcomeCompletionStored && shouldPersistWelcomeCompletion(outcome, this.defaultAccountService)) {
253
welcomeCompletionStored = true;
254
this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
255
}
256
this.overlayRef.clear();
257
this.watchSignInState();
258
});
259
}
260
}
261
262
registerWorkbenchContribution2(SessionsWelcomeContribution.ID, SessionsWelcomeContribution, WorkbenchPhase.BlockRestore);
263
264
registerAction2(class extends Action2 {
265
constructor() {
266
super({
267
id: 'workbench.action.resetSessionsWelcome',
268
title: localize2('resetSessionsWelcome', "Reset Agents Welcome"),
269
category: Categories.Developer,
270
f1: true,
271
});
272
}
273
run(accessor: ServicesAccessor): void {
274
const storageService = accessor.get(IStorageService);
275
const instantiationService = accessor.get(IInstantiationService);
276
const layoutService = accessor.get(IWorkbenchLayoutService);
277
const defaultAccountService = accessor.get(IDefaultAccountService);
278
const contextKeyService = accessor.get(IContextKeyService);
279
const environmentService = accessor.get(IWorkbenchEnvironmentService);
280
const logService = accessor.get(ILogService);
281
resetSessionsWelcome(storageService, instantiationService, layoutService, defaultAccountService, contextKeyService, environmentService, logService);
282
}
283
});
284
285