Skip to content

Commit 6743fb4

Browse files
sandy081Copilot
andauthored
agents: enhance welcome screen for first-launch and signed-in users (#312627)
- Show a personalized welcome greeting with 'Get Started' button for first-launch users who are already signed in - Show enhanced sign-in screen with welcome content for first-launch users who are not yet signed in - Show plain sign-in screen for returning users who are not signed in - Add 'Happy Agentic Coding!' tagline to all welcome screens - Update 'Sign in with GitHub' button label - Add `isFirstLaunch` parameter to `SessionsWalkthroughOverlay` - Guard autorun auto-dismiss with `isShowingWelcome` to prevent race condition where overlay is dismissed before welcome renders - Fix developer reset command to simulate first-launch experience Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 15d2d8c commit 6743fb4

4 files changed

Lines changed: 162 additions & 19 deletions

File tree

src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,43 @@
363363
border-radius: 2px !important;
364364
}
365365

366+
/* ---- Welcome screen (first launch + signed in) ---- */
367+
368+
.sessions-walkthrough-welcome-actions {
369+
display: flex;
370+
flex-direction: column;
371+
align-items: flex-start;
372+
gap: 12px;
373+
margin-top: 16px;
374+
}
375+
376+
.sessions-walkthrough-get-started-btn {
377+
padding: 8px 24px;
378+
border-radius: 6px;
379+
border: none;
380+
background: var(--vscode-button-background);
381+
color: var(--vscode-button-foreground);
382+
font-size: 13px;
383+
font-weight: 500;
384+
cursor: pointer;
385+
transition: background 100ms;
386+
}
387+
388+
.sessions-walkthrough-get-started-btn:hover {
389+
background: var(--vscode-button-hoverBackground);
390+
}
391+
392+
.sessions-walkthrough-get-started-btn:focus-visible {
393+
outline: 2px solid var(--vscode-focusBorder);
394+
outline-offset: 2px;
395+
}
396+
397+
.sessions-walkthrough-tagline {
398+
font-style: italic;
399+
opacity: 0.85;
400+
margin-top: 4px;
401+
}
402+
366403
/* Reduced motion */
367404

368405
.monaco-reduce-motion .sessions-walkthrough-overlay,

src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,22 @@ export class SessionsWalkthroughOverlay extends Disposable {
4949
private currentFocusableElements: readonly HTMLElement[] = [];
5050
private _resolveOutcome!: (outcome: WalkthroughOutcome) => void;
5151
private _outcomeResolved = false;
52+
private _isShowingWelcome = false;
53+
54+
/**
55+
* Whether the overlay is currently displaying the signed-in welcome
56+
* greeting (as opposed to the sign-in provider buttons). When `true`,
57+
* external callers should **not** auto-dismiss the overlay — the user
58+
* is expected to click "Get Started" to proceed.
59+
*/
60+
get isShowingWelcome(): boolean { return this._isShowingWelcome; }
5261

5362
/** Resolves when the user completes or dismisses the walkthrough. */
5463
readonly outcome: Promise<WalkthroughOutcome> = new Promise(resolve => { this._resolveOutcome = resolve; });
5564

5665
constructor(
5766
container: HTMLElement,
67+
private readonly _isFirstLaunch: boolean,
5868
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
5969
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
6070
@ICommandService private readonly commandService: ICommandService,
@@ -102,6 +112,12 @@ export class SessionsWalkthroughOverlay extends Disposable {
102112
this.disclaimerElement = disclaimer.element;
103113
this.disclaimerLinks = disclaimer.links;
104114

115+
// Set synchronously so the autorun in the contribution doesn't
116+
// auto-dismiss before the async _renderSignIn completes.
117+
if (this._isFirstLaunch && this._isAlreadySetUp()) {
118+
this._isShowingWelcome = true;
119+
}
120+
105121
this._renderSignIn();
106122
}
107123

@@ -115,26 +131,42 @@ export class SessionsWalkthroughOverlay extends Disposable {
115131
this.footerContainer.textContent = '';
116132
this.disclaimerElement.classList.toggle('hidden', this.disclaimerLinks.length === 0);
117133

134+
const productName = this.productService.nameLong;
135+
118136
// Horizontal layout: icon left, text + buttons right
119137
const layout = append(this.contentContainer, $('.sessions-walkthrough-hero'));
120138

121139
append(layout, $('div.sessions-walkthrough-logo'));
122140

123141
const right = append(layout, $('.sessions-walkthrough-hero-text'));
124-
const titleEl = append(right, $('h2', undefined, localize('walkthrough.step1.title', "Welcome to Agents")));
125-
const subtitleEl = append(right, $('p', undefined, localize('walkthrough.step1.subtitle', "Sign in to continue with agent-powered development.")));
126142

127-
// If already signed in, finish immediately so the app can render.
128-
if (this._isAlreadySetUp()) {
129-
this.complete();
143+
// First time + signed in → welcome greeting with "Get Started"
144+
if (this._isFirstLaunch && this._isAlreadySetUp()) {
145+
this._renderWelcome(stepDisposables, right, productName);
130146
return;
131147
}
132148

149+
// First time + not signed in → welcome content with sign-in buttons
150+
// Returning + not signed in → plain sign-in screen
151+
const titleEl = this._isFirstLaunch
152+
? append(right, $('h2', undefined, localize('walkthrough.welcome.title', "Welcome to {0}", productName)))
153+
: append(right, $('h2', undefined, localize('walkthrough.signin.title', "Sign In")));
154+
const subtitleEl = append(right, $('p', undefined, this._isFirstLaunch
155+
? localize('walkthrough.welcome.subtitle', "Your AI-powered coding agent that builds, tests, and iterates for you.")
156+
: localize('walkthrough.signin.subtitle', "Sign in to continue.")));
157+
if (this._isFirstLaunch) {
158+
append(right, $('p.sessions-walkthrough-tagline', undefined, localize('walkthrough.welcome.tagline', "Happy Agentic Coding!")));
159+
}
160+
161+
this._renderSignInButtons(stepDisposables, right, titleEl, subtitleEl);
162+
}
163+
164+
private _renderSignInButtons(stepDisposables: DisposableStore, right: HTMLElement, titleEl: HTMLElement, subtitleEl: HTMLElement): void {
133165
const signInActions = append(right, $('.sessions-walkthrough-sign-in-actions'));
134166
const providerRow = append(signInActions, $('.sessions-walkthrough-providers-row'));
135167

136168
const githubBtn = append(providerRow, $('button.sessions-walkthrough-provider-btn.sessions-walkthrough-provider-primary.provider-github')) as HTMLButtonElement;
137-
append(githubBtn, $('span.sessions-walkthrough-provider-label', undefined, localize('walkthrough.signin.github', "Continue with GitHub")));
169+
append(githubBtn, $('span.sessions-walkthrough-provider-label', undefined, localize('walkthrough.signin.github', "Sign in with GitHub")));
138170

139171
// Desktop-only provider buttons
140172
let providerButtons: HTMLButtonElement[];
@@ -202,6 +234,34 @@ export class SessionsWalkthroughOverlay extends Disposable {
202234
}
203235
}
204236

237+
// ------------------------------------------------------------------
238+
// Welcome (first launch + signed in)
239+
240+
private _renderWelcome(stepDisposables: DisposableStore, right: HTMLElement, productName: string): void {
241+
this._isShowingWelcome = true;
242+
this.disclaimerElement.classList.add('hidden');
243+
244+
append(right, $('h2', undefined, localize('walkthrough.welcome.title', "Welcome to {0}", productName)));
245+
append(right, $('p', undefined, localize('walkthrough.welcome.subtitle', "Your AI-powered coding agent that builds, tests, and iterates for you.")));
246+
append(right, $('p.sessions-walkthrough-tagline', undefined, localize('walkthrough.welcome.tagline', "Happy Agentic Coding!")));
247+
248+
const actions = append(right, $('.sessions-walkthrough-welcome-actions'));
249+
const getStartedBtn = append(actions, $('button.sessions-walkthrough-get-started-btn')) as HTMLButtonElement;
250+
getStartedBtn.textContent = localize('walkthrough.welcome.getStarted', "Get Started");
251+
stepDisposables.add(addDisposableListener(getStartedBtn, EventType.CLICK, () => {
252+
this._isShowingWelcome = false;
253+
this.complete();
254+
}));
255+
256+
this.currentFocusableElements = [getStartedBtn];
257+
258+
disposableTimeout(() => {
259+
if (this.overlay.isConnected) {
260+
getStartedBtn.focus();
261+
}
262+
}, 0, stepDisposables);
263+
}
264+
205265
private _isAlreadySetUp(): boolean {
206266
const { sentiment, entitlement } = this.chatEntitlementService;
207267
return !!(

src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,14 @@ export function resetSessionsWelcome(
7575
const walkthrough = store.add(instantiationService.createInstance(
7676
SessionsWalkthroughOverlay,
7777
layoutService.mainContainer,
78+
true,
7879
));
7980

8081
store.add(autorun(reader => {
8182
chatEntitlementService.sentimentObs.read(reader);
8283
chatEntitlementService.entitlementObs.read(reader);
8384

84-
if (!needsChatSetup(chatEntitlementService)) {
85+
if (!walkthrough.isShowingWelcome && !needsChatSetup(chatEntitlementService)) {
8586
storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
8687
walkthrough.complete();
8788
store.dispose();
@@ -141,7 +142,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
141142
}
142143
const isFirstLaunch = !this.storageService.getBoolean(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION, false);
143144
if (isFirstLaunch) {
144-
this.showWalkthrough();
145+
this.showWalkthrough(true);
145146
} else {
146147
this.showWalkthroughIfNeeded();
147148
}
@@ -163,7 +164,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
163164
} catch {
164165
// Provider not available yet — show walkthrough
165166
}
166-
this.showWalkthrough();
167+
this.showWalkthrough(false);
167168
}
168169

169170
/**
@@ -188,13 +189,13 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
188189
}
189190
this.logService.info('[sessions welcome] GitHub session removed on web, re-showing walkthrough');
190191
this.storageService.remove(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION);
191-
this.showWalkthrough();
192+
this.showWalkthrough(false);
192193
}));
193194
}
194195

195196
private showWalkthroughIfNeeded(): void {
196197
if (this._needsChatSetup()) {
197-
this.showWalkthrough();
198+
this.showWalkthrough(false);
198199
} else {
199200
this.watchEntitlementState();
200201
}
@@ -220,7 +221,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
220221
const includeUnknown = !this.storageService.getBoolean(WELCOME_COMPLETE_KEY, StorageScope.APPLICATION, false);
221222
const needsSetup = this._needsChatSetup(includeUnknown);
222223
if (setupComplete && needsSetup) {
223-
this.showWalkthrough();
224+
this.showWalkthrough(false);
224225
}
225226
setupComplete = !needsSetup;
226227
});
@@ -230,7 +231,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
230231
return needsChatSetup(this.chatEntitlementService, includeUnknown);
231232
}
232233

233-
private showWalkthrough(): void {
234+
private showWalkthrough(isFirstLaunch: boolean): void {
234235
if (this.overlayRef.value) {
235236
return;
236237
}
@@ -247,6 +248,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
247248
const walkthrough = this.overlayRef.value.add(this.instantiationService.createInstance(
248249
SessionsWalkthroughOverlay,
249250
this.layoutService.mainContainer,
251+
isFirstLaunch,
250252
));
251253

252254
// When chat setup completes (observables flip), persist completion and
@@ -255,7 +257,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
255257
this.chatEntitlementService.sentimentObs.read(reader);
256258
this.chatEntitlementService.entitlementObs.read(reader);
257259

258-
if (!welcomeCompletionStored && !this._needsChatSetup()) {
260+
if (!welcomeCompletionStored && !walkthrough.isShowingWelcome && !this._needsChatSetup()) {
259261
welcomeCompletionStored = true;
260262
this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
261263
walkthrough.complete();

src/vs/sessions/contrib/welcome/test/browser/welcome.contribution.test.ts

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,50 @@ suite('SessionsWelcomeContribution', () => {
279279
assert.strictEqual(isOverlayVisible(), false, 'should dismiss once setup completes');
280280
});
281281

282+
(isWeb ? test.skip : test)('first-launch + already signed in shows welcome screen; Get Started completes it', async () => {
283+
// Already set up: installed, not disabled, has entitlement
284+
mockEntitlementService.entitlementObs.set(ChatEntitlement.Free, undefined);
285+
mockEntitlementService.sentimentObs.set({ completed: true, installed: true } as IChatSentiment, undefined);
286+
287+
instantiationService.stub(IDefaultAccountService, {
288+
getDefaultAccount: () => Promise.resolve(undefined)
289+
} as unknown as IDefaultAccountService);
290+
instantiationService.stub(ILogService, new NullLogService());
291+
instantiationService.stub(ICommandService, {
292+
executeCommand: () => Promise.resolve(false)
293+
} as unknown as ICommandService);
294+
instantiationService.stub(IExtensionService, {
295+
stopExtensionHosts: () => Promise.resolve(false),
296+
startExtensionHosts: () => Promise.resolve()
297+
} as unknown as IExtensionService);
298+
299+
const container = document.createElement('div');
300+
document.body.appendChild(container);
301+
302+
try {
303+
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container, true));
304+
305+
assert.strictEqual(overlay.isShowingWelcome, true, 'should be in welcome mode');
306+
assert.ok(container.querySelector('.sessions-walkthrough-get-started-btn'), 'should show Get Started button');
307+
assert.strictEqual(container.querySelector('.sessions-walkthrough-provider-btn'), null, 'should not show sign-in buttons');
308+
309+
let outcomeResolved = false;
310+
overlay.outcome.then(() => { outcomeResolved = true; });
311+
312+
const getStartedBtn = container.querySelector<HTMLButtonElement>('.sessions-walkthrough-get-started-btn');
313+
assert.ok(getStartedBtn);
314+
getStartedBtn.click();
315+
await flushMicrotasks();
316+
317+
assert.strictEqual(overlay.isShowingWelcome, false, 'isShowingWelcome should be cleared after Get Started');
318+
assert.strictEqual(outcomeResolved, true, 'outcome should resolve after Get Started click');
319+
320+
overlay.dispose();
321+
} finally {
322+
container.remove();
323+
}
324+
});
325+
282326
test('walkthrough cannot be dismissed by Escape or backdrop click', () => {
283327
mockEntitlementService.entitlementObs.set(ChatEntitlement.Unknown, undefined);
284328
mockEntitlementService.sentimentObs.set({ installed: false } as IChatSentiment, undefined);
@@ -299,7 +343,7 @@ suite('SessionsWelcomeContribution', () => {
299343
document.body.appendChild(container);
300344

301345
try {
302-
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container));
346+
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container, true));
303347
const overlayElement = container.querySelector<HTMLElement>('.sessions-walkthrough-overlay');
304348
assert.ok(overlayElement);
305349

@@ -349,7 +393,7 @@ suite('SessionsWelcomeContribution', () => {
349393
}
350394
} as unknown as ICommandService);
351395

352-
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container));
396+
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container, true));
353397
const githubButton = container.querySelector<HTMLButtonElement>('.sessions-walkthrough-provider-btn.provider-github');
354398
const googleButton = container.querySelector<HTMLButtonElement>('.sessions-walkthrough-provider-btn.provider-google');
355399
const appleButton = container.querySelector<HTMLButtonElement>('.sessions-walkthrough-provider-btn.provider-apple');
@@ -406,7 +450,7 @@ suite('SessionsWelcomeContribution', () => {
406450
document.body.appendChild(container);
407451

408452
try {
409-
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container));
453+
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container, true));
410454
const enterpriseButton = container.querySelector<HTMLButtonElement>('.sessions-walkthrough-provider-btn.provider-enterprise');
411455
assert.ok(enterpriseButton);
412456

@@ -455,7 +499,7 @@ suite('SessionsWelcomeContribution', () => {
455499
document.body.appendChild(container);
456500

457501
try {
458-
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container));
502+
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container, true));
459503
const disclaimer = container.querySelector<HTMLElement>('.sessions-walkthrough-disclaimer');
460504
assert.ok(disclaimer);
461505
assert.strictEqual(disclaimer.classList.contains('hidden'), false);
@@ -499,7 +543,7 @@ suite('SessionsWelcomeContribution', () => {
499543
document.body.appendChild(container);
500544

501545
try {
502-
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container));
546+
const overlay = disposables.add(instantiationService.createInstance(SessionsWalkthroughOverlay, container, true));
503547
const disclaimer = container.querySelector<HTMLElement>('.sessions-walkthrough-disclaimer');
504548
assert.ok(disclaimer);
505549
assert.strictEqual(disclaimer.classList.contains('hidden'), false);

0 commit comments

Comments
 (0)