Change:
Testing Continue from web inside desktop Let the window sit Install update (which restarts the App) you are instantly greeted with a modal dialog (I wasn't even in that window of VS Code) Ideas don't use a modal but the custom modal overlay which is more friendly and doesn't steep focus don't show this when my local changes are no different to those on the server <img width="1391" alt="Screenshot 2022-08-24 at 09 26 13" src="https://user-"
Is the custom modal overlay the same thing as the custom modal dialog, i.e. :+1: I filed for this.

Discussion:
import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { URI } from 'vs/base/common/uri'; <del> import { joinPath, relativePath } from 'vs/base/common/resources'; </del> <ins> import { basename, joinPath, relativePath } from 'vs/base/common/resources'; </ins> import { encodeBase64 } from 'vs/base/common/buffer'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';

import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; <del> import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; </del> <ins> import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { getFileNamesMessage, IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; </ins> import { IProductService } from 'vs/platform/product/common/productService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';

import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IOutputService } from 'vs/workbench/services/output/common/output'; import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; <ins> import { sha1Hex } from 'vs/base/browser/hash'; </ins> registerSingleton(IEditSessionsLogService, EditSessionsLogService, false); registerSingleton(IEditSessionsStorageService, EditSessionsWorkbenchService, false);

} try { <del> const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = []; let hasLocalUncommittedChanges = false; const workspaceFolders = this.contextService.getWorkspace().folders; for (const folder of editSession.folders) { const cancellationTokenSource = new CancellationTokenSource(); let folderRoot: IWorkspaceFolder | undefined; if (folder.canonicalIdentity) { // Look for an edit session identifier that we can use for (const f of workspaceFolders) { const identity = await this.editSessionIdentityService.getEditSessionIdentifier(f, cancellationTokenSource); this.logService.info(`Matching identity ${identity} against edit session folder identity ${folder.canonicalIdentity}...`); if (equals(identity, folder.canonicalIdentity)) { folderRoot = f; break; } } } else { folderRoot = workspaceFolders.find((f) => f.name === folder.name); } if (!folderRoot) { this.logService.info(`Skipping applying ${folder.workingChanges.length} changes from edit session with ref ${ref} as no matching workspace folder was found.`); return; } for (const repository of this.scmService.repositories) { if (repository.provider.rootUri !== undefined && this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name === folder.name && this.getChangedResources(repository).length > 0 ) { hasLocalUncommittedChanges = true; break; } } for (const { relativeFilePath, contents, type } of folder.workingChanges) { const uri = joinPath(folderRoot.uri, relativeFilePath); changes.push({ uri: uri, type: type, contents: contents }); } } </del> <ins> const { changes, conflictingChanges } = await this.generateChanges(editSession, ref); // TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents if (conflictingChanges.length > 0) { const yes = localize('resume edit session yes', 'Yes'); const cancel = localize('resume edit session cancel', 'Cancel'); // Allow to show edit sessions const result = await this.dialogService.show( Severity.Warning, changes.length > 1 ? localize('resume edit session warning many', 'Resuming your edit session will overwrite the following {0} files. Do you want to proceed?', changes.length) : localize('resume edit session warning 1', 'Resuming your edit session will overwrite {0}. Do you want to proceed?', basename(changes[0].uri)), [cancel, yes], { custom: true, detail: changes.length > 1 ? getFileNamesMessage(conflictingChanges.map((c) => c.uri)) : undefined, cancelId: 0 }); </ins> <del> if (hasLocalUncommittedChanges) { // TODO@joyceerhl Provide the option to diff files which would be overwritten by edit session contents const result = await this.dialogService.confirm({ message: localize('resume edit session warning', 'Resuming your edit session may overwrite your existing uncommitted changes. Do you want to proceed?'), type: 'warning', title: EDIT_SESSION_SYNC_CATEGORY.value }); if (!result.confirmed) { </del> <ins> if (result.choice === 0) { </ins> return; } }

} } <ins> private async generateChanges(editSession: EditSession, ref: string) { const changes: ({ uri: URI; type: ChangeType; contents: string | undefined })[] = []; const conflictingChanges = []; const workspaceFolders = this.contextService.getWorkspace().folders; for (const folder of editSession.folders) { const cancellationTokenSource = new CancellationTokenSource(); let folderRoot: IWorkspaceFolder | undefined; if (folder.canonicalIdentity) { // Look for an edit session identifier that we can use for (const f of workspaceFolders) { const identity = await this.editSessionIdentityService.getEditSessionIdentifier(f, cancellationTokenSource); this.logService.info(`Matching identity ${identity} against edit session folder identity ${folder.canonicalIdentity}...`); if (equals(identity, folder.canonicalIdentity)) { folderRoot = f; break; } } } else { folderRoot = workspaceFolders.find((f) => f.name === folder.name); } if (!folderRoot) { this.logService.info(`Skipping applying ${folder.workingChanges.length} changes from edit session with ref ${ref} as no matching workspace folder was found.`); return { changes: [], conflictingChanges: [] }; } const localChanges = new Set<string>(); for (const repository of this.scmService.repositories) { if (repository.provider.rootUri !== undefined && this.contextService.getWorkspaceFolder(repository.provider.rootUri)?.name === folder.name ) { const repositoryChanges = this.getChangedResources(repository); repositoryChanges.forEach((change) => localChanges.add(change.toString())); } } for (const change of folder.workingChanges) { const uri = joinPath(folderRoot.uri, change.relativeFilePath); changes.push({ uri, type: change.type, contents: change.contents }); if (await this.willChangeLocalContents(localChanges, uri, change)) { conflictingChanges.push({ uri, type: change.type, contents: change.contents }); } } } return { changes, conflictingChanges }; } private async willChangeLocalContents(localChanges: Set<string>, uriWithIncomingChanges: URI, incomingChange: Change) { if (!localChanges.has(uriWithIncomingChanges.toString())) { return false; } const { contents, type } = incomingChange; switch (type) { case (ChangeType.Addition): { const [originalContents, incomingContents] = await Promise.all([sha1Hex(contents), sha1Hex(encodeBase64((await this.fileService.readFile(uriWithIncomingChanges)).value))]); return originalContents !== incomingContents; } case (ChangeType.Deletion): { return await this.fileService.exists(uriWithIncomingChanges); } default: throw new Error('Unhandled change type.'); } } </ins> async storeEditSession(fromStoreCommand: boolean): Promise<string | undefined> { const folders: Folder[] = []; let hasEdits = false;

} private getChangedResources(repository: ISCMRepository) { <del> const trackedUris = repository.provider.groups.elements.reduce((resources, resourceGroups) => { </del> <ins> return repository.provider.groups.elements.reduce((resources, resourceGroups) => { </ins> resourceGroups.elements.forEach((resource) => resources.add(resource.sourceUri)); return resources; }, new Set<URI>()); // A URI might appear in more than one resource group <del> return [...trackedUris]; </del> } private hasEditSession() { for (const repository of this.scmService.repositories) { <del> if (this.getChangedResources(repository).length > 0) { </del> <ins> if (this.getChangedResources(repository).size > 0) { </ins> return true; } }

import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; <ins> import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; </ins> const folderName = 'test-folder'; const folderUri = URI.file(`/${folderName}`);

instantiationService.stub(IProgressService, ProgressService); instantiationService.stub(ISCMService, SCMService); instantiationService.stub(IEnvironmentService, TestEnvironmentService); <ins> instantiationService.stub(IDialogService, new class extends mock<IDialogService>() { override async show() { return { choice: 1 }; } }); </ins> instantiationService.stub(IConfigurationService, new TestConfigurationService({ workbench: { experimental: { editSessions: { enabled: true } } } })); instantiationService.stub(IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() { override getWorkspace() {

const readStub = sandbox.stub().returns({ editSession, ref: '0' }); instantiationService.stub(IEditSessionsStorageService, 'read', readStub); <ins> // Ensure that user does not get prompted here const dialogServiceShowStub = sandbox.stub(); instantiationService.stub(IDialogService, 'show', dialogServiceShowStub); </ins> // Create root folder await fileService.createFolder(folderUri);

// Verify edit session was correctly applied assert.equal((await fileService.readFile(fileUri)).value.toString(), fileContents); <ins> assert.equal(dialogServiceShowStub.called, false); </ins> }); test('Edit session not stored if there are no edits', async function () {