Skip to content

Commit fd640bb

Browse files
joshspicerCopilot
andauthored
sessions: show welcome overlay on explicit sign-out (#307191)
* sessions: show welcome overlay on explicit sign-out When the user signs out via the account menu, show the welcome/login overlay immediately instead of waiting for the next app launch. Listen to IDefaultAccountService.onDidChangeDefaultAccount for the account becoming null, which reliably indicates a user-initiated sign-out. Token refreshes keep the account non-null so there is no false-positive risk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: stub IDefaultAccountService in welcome contribution tests The tests were missing a mock for IDefaultAccountService which was newly injected. Add an emitter-based stub and a test that verifies the explicit sign-out flow shows the overlay. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 998909d commit fd640bb

2 files changed

Lines changed: 52 additions & 9 deletions

File tree

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

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import './media/welcomeOverlay.css';
77
import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
88
import { SessionsWelcomeVisibleContext } from '../../../common/contextkeys.js';
99
import { $, append } from '../../../../base/browser/dom.js';
10-
import { autorun } from '../../../../base/common/observable.js';
10+
import { autorun, observableValue } from '../../../../base/common/observable.js';
1111
import { Codicon } from '../../../../base/common/codicons.js';
1212
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
1313
import { Button } from '../../../../base/browser/ui/button/button.js';
@@ -28,6 +28,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
2828
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
2929
import { IWorkbenchEnvironmentService } from '../../../../workbench/services/environment/common/environmentService.js';
3030
import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js';
31+
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
3132

3233
const WELCOME_COMPLETE_KEY = 'workbench.agentsession.welcomeComplete';
3334

@@ -136,6 +137,14 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
136137
private readonly overlayRef = this._register(new MutableDisposable<DisposableStore>());
137138
private readonly watcherRef = this._register(new MutableDisposable());
138139

140+
/**
141+
* Tracks whether the user has explicitly signed out. Used to include
142+
* {@link ChatEntitlement.Unknown} in setup checks only after a genuine
143+
* sign-out (account removed), avoiding false positives from token refreshes
144+
* where the account stays non-null.
145+
*/
146+
private readonly signedOut = observableValue(this, false);
147+
139148
constructor(
140149
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
141150
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
@@ -144,6 +153,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
144153
@IStorageService private readonly storageService: IStorageService,
145154
@IContextKeyService private readonly contextKeyService: IContextKeyService,
146155
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
156+
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
147157
) {
148158
super();
149159

@@ -183,23 +193,33 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc
183193
* completed. If the user's state changes such that setup is needed again
184194
* (e.g. extension uninstalled/disabled), shows the welcome overlay.
185195
*
186-
* {@link ChatEntitlement.Unknown} is intentionally ignored here: it is
187-
* almost always a transient state caused by a stale OAuth token being
188-
* refreshed after an update. A genuine sign-out will be caught on the
189-
* next app launch via the initial {@link showOverlayIfNeeded} check.
196+
* {@link ChatEntitlement.Unknown} is intentionally ignored unless the
197+
* default account has been removed, which reliably indicates a genuine
198+
* user-initiated sign-out rather than a transient token refresh (where
199+
* the account object stays non-null).
190200
*/
191201
private watchEntitlementState(): void {
202+
this.signedOut.set(false, undefined);
192203
let setupComplete = !this._needsChatSetup(false);
193-
this.watcherRef.value = autorun(reader => {
204+
const store = new DisposableStore();
205+
206+
store.add(this.defaultAccountService.onDidChangeDefaultAccount(account => {
207+
this.signedOut.set(account === null, undefined);
208+
}));
209+
210+
store.add(autorun(reader => {
194211
this.chatEntitlementService.sentimentObs.read(reader);
195212
this.chatEntitlementService.entitlementObs.read(reader);
213+
const isSignedOut = this.signedOut.read(reader);
196214

197-
const needsSetup = this._needsChatSetup(false);
215+
const needsSetup = this._needsChatSetup(isSignedOut);
198216
if (setupComplete && needsSetup) {
199217
this.showOverlay();
200218
}
201219
setupComplete = !needsSetup;
202-
});
220+
}));
221+
222+
this.watcherRef.value = store;
203223
}
204224

205225
private _needsChatSetup(includeUnknown: boolean = true): boolean {

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import assert from 'assert';
7-
import { Event } from '../../../../../base/common/event.js';
7+
import { Emitter, Event } from '../../../../../base/common/event.js';
88
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
99
import { ISettableObservable, observableValue, transaction } from '../../../../../base/common/observable.js';
10+
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
1011
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
1112
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
1213
import { IProductService } from '../../../../../platform/product/common/productService.js';
@@ -53,12 +54,16 @@ suite('SessionsWelcomeContribution', () => {
5354
const disposables = new DisposableStore();
5455
let instantiationService: TestInstantiationService;
5556
let mockEntitlementService: MockChatEntitlementService;
57+
let defaultAccountEmitter: Emitter<unknown>;
5658

5759
setup(() => {
5860
instantiationService = workbenchInstantiationService(undefined, disposables);
5961
mockEntitlementService = new MockChatEntitlementService();
6062
instantiationService.stub(IChatEntitlementService, mockEntitlementService as unknown as IChatEntitlementService);
6163

64+
defaultAccountEmitter = disposables.add(new Emitter<unknown>());
65+
instantiationService.stub(IDefaultAccountService, { onDidChangeDefaultAccount: defaultAccountEmitter.event } as Partial<IDefaultAccountService> as IDefaultAccountService);
66+
6267
// Ensure product has a defaultChatAgent so the contribution activates
6368
const productService = instantiationService.get(IProductService);
6469
instantiationService.stub(IProductService, {
@@ -206,4 +211,22 @@ suite('SessionsWelcomeContribution', () => {
206211

207212
assert.strictEqual(isOverlayVisible(), true, 'should show overlay for Available entitlement');
208213
});
214+
215+
test('returning user: explicit sign-out shows overlay', () => {
216+
markReturningUser();
217+
mockEntitlementService.entitlementObs.set(ChatEntitlement.Free, undefined);
218+
mockEntitlementService.sentimentObs.set({ completed: true } as IChatSentiment, undefined);
219+
220+
const contribution = disposables.add(instantiationService.createInstance(SessionsWelcomeContribution));
221+
assert.ok(contribution);
222+
assert.strictEqual(isOverlayVisible(), false, 'should not show initially');
223+
224+
// Simulate explicit sign-out: account removed then entitlement goes Unknown
225+
defaultAccountEmitter.fire(null);
226+
transaction(tx => {
227+
mockEntitlementService.entitlementObs.set(ChatEntitlement.Unknown, tx);
228+
});
229+
230+
assert.strictEqual(isOverlayVisible(), true, 'should show overlay after explicit sign-out');
231+
});
209232
});

0 commit comments

Comments
 (0)