Skip to content

Commit 2a42323

Browse files
authored
Fix Continue Edit Session authentication flow and add error handling (microsoft#151975)
* Handle failing to store edit session * Add action to reset authentication state * List authentication providers for edit session sync * Resolve quickpick promise if no account selected
1 parent 1a07fd1 commit 2a42323

File tree

3 files changed

+104
-19
lines changed

3 files changed

+104
-19
lines changed

src/vs/workbench/contrib/sessionSync/browser/sessionSync.contribution.ts

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
1010
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
1111
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
1212
import { localize } from 'vs/nls';
13-
import { ISessionSyncWorkbenchService, Change, ChangeType, Folder, EditSession, FileType } from 'vs/workbench/services/sessionSync/common/sessionSync';
13+
import { ISessionSyncWorkbenchService, Change, ChangeType, Folder, EditSession, FileType, EDIT_SESSION_SYNC_TITLE } from 'vs/workbench/services/sessionSync/common/sessionSync';
1414
import { ISCMService } from 'vs/workbench/contrib/scm/common/scm';
1515
import { IFileService } from 'vs/platform/files/common/files';
1616
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
@@ -21,17 +21,19 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
2121
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
2222
import { SessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService';
2323
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
24+
import { UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
25+
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
26+
import { INotificationService } from 'vs/platform/notification/common/notification';
2427

2528
registerSingleton(ISessionSyncWorkbenchService, SessionSyncWorkbenchService);
2629

27-
const SYNC_TITLE = localize('session sync', 'Edit Sessions');
2830
const applyLatestCommand = {
2931
id: 'workbench.sessionSync.actions.applyLatest',
30-
title: localize('apply latest', "{0}: Apply Latest Edit Session", SYNC_TITLE),
32+
title: localize('apply latest', "{0}: Apply Latest Edit Session", EDIT_SESSION_SYNC_TITLE),
3133
};
3234
const storeLatestCommand = {
3335
id: 'workbench.sessionSync.actions.storeLatest',
34-
title: localize('store latest', "{0}: Store Latest Edit Session", SYNC_TITLE),
36+
title: localize('store latest', "{0}: Store Latest Edit Session", EDIT_SESSION_SYNC_TITLE),
3537
};
3638

3739
export class SessionSyncContribution extends Disposable implements IWorkbenchContribution {
@@ -42,7 +44,9 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
4244
@ISessionSyncWorkbenchService private readonly sessionSyncWorkbenchService: ISessionSyncWorkbenchService,
4345
@IFileService private readonly fileService: IFileService,
4446
@IProgressService private readonly progressService: IProgressService,
47+
@ITelemetryService private readonly telemetryService: ITelemetryService,
4548
@ISCMService private readonly scmService: ISCMService,
49+
@INotificationService private readonly notificationService: INotificationService,
4650
@IConfigurationService private configurationService: IConfigurationService,
4751
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
4852
) {
@@ -177,7 +181,29 @@ export class SessionSyncContribution extends Disposable implements IWorkbenchCon
177181

178182
const data: EditSession = { folders, version: 1 };
179183

180-
await this.sessionSyncWorkbenchService.write(data);
184+
try {
185+
await this.sessionSyncWorkbenchService.write(data);
186+
} catch (ex) {
187+
type UploadFailedEvent = { reason: string };
188+
type UploadFailedClassification = {
189+
owner: 'joyceerhl'; comment: 'Reporting when Continue On server request fails.';
190+
reason?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The reason that the server request failed.' };
191+
};
192+
193+
if (ex instanceof UserDataSyncStoreError) {
194+
switch (ex.code) {
195+
case UserDataSyncErrorCode.TooLarge:
196+
// Uploading a payload can fail due to server size limits
197+
this.telemetryService.publicLog2<UploadFailedEvent, UploadFailedClassification>('sessionSync.upload.failed', { reason: 'TooLarge' });
198+
this.notificationService.error(localize('payload too large', 'Your edit session exceeds the size limit and cannot be stored.'));
199+
break;
200+
default:
201+
this.telemetryService.publicLog2<UploadFailedEvent, UploadFailedClassification>('sessionSync.upload.failed', { reason: 'unknown' });
202+
this.notificationService.error(localize('payload failed', 'Your edit session cannot be stored.'));
203+
break;
204+
}
205+
}
206+
}
181207
}
182208
}
183209

src/vs/workbench/services/sessionSync/browser/sessionSyncWorkbenchService.ts

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@
66
import { Disposable } from 'vs/base/common/lifecycle';
77
import { URI } from 'vs/base/common/uri';
88
import { localize } from 'vs/nls';
9+
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
910
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
1011
import { IFileService } from 'vs/platform/files/common/files';
1112
import { ILogService } from 'vs/platform/log/common/log';
1213
import { IProductService } from 'vs/platform/product/common/productService';
13-
import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
14+
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
1415
import { IRequestService } from 'vs/platform/request/common/request';
1516
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
1617
import { IAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync';
1718
import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
1819
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication';
1920
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
20-
import { EditSession, ISessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/common/sessionSync';
21+
import { EditSession, EDIT_SESSION_SYNC_TITLE, ISessionSyncWorkbenchService } from 'vs/workbench/services/sessionSync/common/sessionSync';
22+
23+
type ExistingSession = IQuickPickItem & { session: AuthenticationSession & { providerId: string } };
24+
type AuthenticationProviderOption = IQuickPickItem & { provider: IAuthenticationProvider };
2125

2226
export class SessionSyncWorkbenchService extends Disposable implements ISessionSyncWorkbenchService {
2327

@@ -49,6 +53,8 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS
4953

5054
// If another window changes the preferred session storage, reset our cached auth state in memory
5155
this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorage(e)));
56+
57+
this.registerResetAuthenticationAction();
5258
}
5359

5460
/**
@@ -58,7 +64,7 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS
5864
async write(editSession: EditSession): Promise<void> {
5965
this.initialized = await this.waitAndInitialize();
6066
if (!this.initialized) {
61-
throw new Error('Unable to store edit session.');
67+
throw new Error('Please sign in to store your edit session.');
6268
}
6369

6470
await this.storeClient?.write('editSessions', JSON.stringify(editSession), null);
@@ -71,7 +77,7 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS
7177
async read(): Promise<EditSession | undefined> {
7278
this.initialized = await this.waitAndInitialize();
7379
if (!this.initialized) {
74-
throw new Error('Unable to apply latest edit session.');
80+
throw new Error('Please sign in to apply your latest edit session.');
7581
}
7682

7783
// Pull latest session data from service
@@ -134,30 +140,57 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS
134140
* Prompts the user to pick an authentication option for storing and getting edit sessions.
135141
*/
136142
private async getAccountPreference(): Promise<AuthenticationSession & { providerId: string } | undefined> {
137-
const quickpick = this.quickInputService.createQuickPick<IQuickPickItem & { session: AuthenticationSession & { providerId: string } }>();
143+
const quickpick = this.quickInputService.createQuickPick<ExistingSession | AuthenticationProviderOption>();
138144
quickpick.title = localize('account preference', 'Edit Sessions');
139145
quickpick.ok = false;
140146
quickpick.placeholder = localize('choose account placeholder', "Select an account to sign in");
141147
quickpick.ignoreFocusOut = true;
142-
// TODO@joyceerhl Should we be showing sessions here?
143-
quickpick.items = await this.getAllSessions();
148+
quickpick.items = await this.createQuickpickItems();
144149

145150
return new Promise((resolve, reject) => {
146-
quickpick.onDidHide((e) => quickpick.dispose());
147-
quickpick.onDidAccept((e) => {
148-
resolve(quickpick.selectedItems[0].session);
151+
quickpick.onDidHide((e) => {
152+
resolve(undefined);
153+
quickpick.dispose();
154+
});
155+
156+
quickpick.onDidAccept(async (e) => {
157+
const selection = quickpick.selectedItems[0];
158+
const session = 'provider' in selection ? { ...await this.authenticationService.createSession(selection.provider.id, selection.provider.scopes), providerId: selection.provider.id } : selection.session;
159+
resolve(session);
149160
quickpick.hide();
150161
});
162+
151163
quickpick.show();
152164
});
153165
}
154166

167+
private async createQuickpickItems(): Promise<(ExistingSession | AuthenticationProviderOption | IQuickPickSeparator)[]> {
168+
const options: (ExistingSession | AuthenticationProviderOption | IQuickPickSeparator)[] = [];
169+
170+
options.push({ type: 'separator', label: localize('signed in', "Signed In") });
171+
172+
const sessions = await this.getAllSessions();
173+
options.push(...sessions);
174+
175+
options.push({ type: 'separator', label: localize('others', "Others") });
176+
177+
for (const authenticationProvider of (await this.getAuthenticationProviders())) {
178+
const signedInForProvider = sessions.some(account => account.session.providerId === authenticationProvider.id);
179+
if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) {
180+
const providerName = this.authenticationService.getLabel(authenticationProvider.id);
181+
options.push({ label: localize('sign in using account', "Sign in with {0}", providerName), provider: authenticationProvider });
182+
}
183+
}
184+
185+
return options;
186+
}
187+
155188
/**
156189
*
157190
* Returns all authentication sessions available from {@link getAuthenticationProviders}.
158191
*/
159192
private async getAllSessions() {
160-
const options = [];
193+
const options: ExistingSession[] = [];
161194
const authenticationProviders = await this.getAuthenticationProviders();
162195

163196
for (const provider of authenticationProviders) {
@@ -226,11 +259,34 @@ export class SessionSyncWorkbenchService extends Disposable implements ISessionS
226259
}
227260
}
228261

262+
private clearAuthenticationPreference(): void {
263+
this.#authenticationInfo = undefined;
264+
this.initialized = false;
265+
this.existingSessionId = undefined;
266+
}
267+
229268
private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void {
230269
if (this.#authenticationInfo?.sessionId && e.removed.find(session => session.id === this.#authenticationInfo?.sessionId)) {
231-
this.#authenticationInfo = undefined;
232-
this.existingSessionId = undefined;
233-
this.initialized = false;
270+
this.clearAuthenticationPreference();
234271
}
235272
}
273+
274+
private registerResetAuthenticationAction() {
275+
const that = this;
276+
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
277+
constructor() {
278+
super({
279+
id: 'workbench.sessionSync.actions.resetAuth',
280+
title: localize('reset auth', '{0}: Reset Authentication State', EDIT_SESSION_SYNC_TITLE),
281+
menu: {
282+
id: MenuId.CommandPalette,
283+
}
284+
});
285+
}
286+
287+
run() {
288+
that.clearAuthenticationPreference();
289+
}
290+
}));
291+
}
236292
}

src/vs/workbench/services/sessionSync/common/sessionSync.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { localize } from 'vs/nls';
67
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
78

9+
export const EDIT_SESSION_SYNC_TITLE = localize('session sync', 'Edit Sessions');
10+
811
export const ISessionSyncWorkbenchService = createDecorator<ISessionSyncWorkbenchService>('ISessionSyncWorkbenchService');
912
export interface ISessionSyncWorkbenchService {
1013
_serviceBrand: undefined;

0 commit comments

Comments
 (0)