workspace: add suppressConfirmation option to updateWorkspaceFolders#306519
workspace: add suppressConfirmation option to updateWorkspaceFolders#306519RockyWearsAHat wants to merge 7 commits intomicrosoft:mainfrom
Conversation
Add an UpdateWorkspaceFoldersOptions interface with a suppressConfirmation flag that allows extensions to skip the workspace-transition confirmation dialog when programmatically updating workspace folders. The change threads the option through the full call chain: - vscode.d.ts: new overload and UpdateWorkspaceFoldersOptions interface - extHost.api.impl.ts: overload detection (options bag vs folder object) - extHostWorkspace.ts: forwards suppressConfirmation via IPC - extHost.protocol.ts: flat boolean across the IPC boundary - mainThreadWorkspace.ts: constructs IEnterWorkspaceOptions for the service - IWorkspaceEditingService: new IEnterWorkspaceOptions interface - abstractWorkspaceEditingService.ts: threads options through updateFolders - electron-browser workspaceEditingService.ts: passes force flag to stopExtensionHosts when suppressConfirmation is true - IExtensionService.stopExtensionHosts: new force parameter that bypasses the extension host veto chain Ref: microsoft#306495
📬 CODENOTIFYThe following users are being notified based on files changed in this PR: @bpaseroMatched files:
|
There was a problem hiding this comment.
Pull request overview
Adds a new suppressConfirmation option to vscode.workspace.updateWorkspaceFolders() so extensions can programmatically trigger workspace-folder transitions without being blocked by the extension-host-restart confirmation dialog, threading the option through the ext host → main thread → workspace editing → extension service stop flow.
Changes:
- Introduces
UpdateWorkspaceFoldersOptions+ a newupdateWorkspaceFoldersoverload invscode.d.ts. - Propagates
suppressConfirmationacross the ext host IPC boundary and into workspace editing services via a newIEnterWorkspaceOptions. - Extends
IExtensionService.stopExtensionHostswith aforceparameter intended to bypass the confirmation/veto flow.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vscode-dts/vscode.d.ts | Adds UpdateWorkspaceFoldersOptions and a new overload to surface suppressConfirmation to extensions. |
| src/vs/workbench/api/common/extHost.api.impl.ts | Implements overload detection and forwards suppressConfirmation into extHost workspace logic. |
| src/vs/workbench/api/common/extHostWorkspace.ts | Threads suppressConfirmation to the main thread IPC call for workspace folder updates. |
| src/vs/workbench/api/common/extHost.protocol.ts | Extends the main-thread workspace RPC signature with suppressConfirmation?: boolean. |
| src/vs/workbench/api/browser/mainThreadWorkspace.ts | Receives suppressConfirmation and forwards it into IWorkspaceEditingService.updateFolders as options. |
| src/vs/workbench/services/workspaces/common/workspaceEditing.ts | Defines IEnterWorkspaceOptions and adds optional options params to workspace-editing service APIs. |
| src/vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService.ts | Adds options plumbing in updateFolders and forwards options through createAndEnterWorkspace/enterWorkspace signatures. |
| src/vs/workbench/services/workspaces/browser/workspaceEditingService.ts | Updates browser implementation signature to conform to new enterWorkspace(..., options?). |
| src/vs/workbench/services/workspaces/electron-browser/workspaceEditingService.ts | Uses options?.suppressConfirmation to invoke extension host stopping with force. |
| src/vs/workbench/services/extensions/common/extensions.ts | Extends IExtensionService.stopExtensionHosts API with a force?: boolean parameter. |
| src/vs/workbench/services/extensions/common/abstractExtensionService.ts | Implements force behavior in stopExtensionHosts by bypassing the veto path. |
Address review feedback: - stopExtensionHosts now forwards force to _doStopExtensionHostsWithVeto instead of bypassing onWillStop entirely, so state persistence hooks run - After awaiting vetos, force mode stops unconditionally (ignoring results) - Thread options through doAddFolders and removeFolders so suppressConfirmation works on all updateFolders code paths - Update JSDoc to reflect that onWillStop listeners still fire
34c47fe to
72a11c5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/vs/workbench/api/common/extHostWorkspace.ts:289
- Adding
suppressConfirmation?: booleanas a positional parameter before the rest arg changes the calling convention: any existing internal caller that doesupdateWorkspaceFolders(ext, 0, 0, folder)will now treatfolderassuppressConfirmationand add zero folders. Please update all direct call sites (notablysrc/vs/workbench/api/test/browser/extHostWorkspace.test.ts) to passundefinedfor the new parameter, or refactor to avoid a positional insert that can be miscalled.
updateWorkspaceFolders(extension: IExtensionDescription, index: number, deleteCount: number, suppressConfirmation?: boolean, ...workspaceFoldersToAdd: { uri: vscode.Uri; name?: string }[]): boolean {
const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri; name?: string }[] = [];
if (Array.isArray(workspaceFoldersToAdd)) {
workspaceFoldersToAdd.forEach(folderToAdd => {
if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) {
- extensionService.test.ts: verify stopExtensionHosts with force flag bypasses sync and async vetos, disposes hosts, and fires onWillStop - extHostWorkspace.test.ts: verify suppressConfirmation parameter is forwarded through the proxy to $updateWorkspaceFolders
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/vs/workbench/api/common/extHostWorkspace.ts:289
- Changing
updateWorkspaceFoldersto takesuppressConfirmationas the 4th parameter breaks existing internal call sites that pass folders as the 4th argument (e.g. existing tests and any other direct callers): the first folder object will be treated assuppressConfirmation, resulting in no folders being added and the call returningfalse(no-op). To avoid this regression, add runtime overload handling (e.g. if the 4th arg is not a boolean, treat it as the first folder to add) and/or update all direct callers accordingly.
updateWorkspaceFolders(extension: IExtensionDescription, index: number, deleteCount: number, suppressConfirmation?: boolean, ...workspaceFoldersToAdd: { uri: vscode.Uri; name?: string }[]): boolean {
const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri; name?: string }[] = [];
if (Array.isArray(workspaceFoldersToAdd)) {
workspaceFoldersToAdd.forEach(folderToAdd => {
if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) {
Match the updated IExtensionService interface that now accepts (reason, auto?, force?) parameters.
When an extension calls updateWorkspaceFolders with suppressConfirmation: true for the first time, a one-time consent dialog asks the user to approve. The decision is persisted per-extension in application-scoped storage. This prevents any extension from silently redirecting the user's workspace without explicit consent, addressing the security concern that the raw suppressConfirmation bypass could be abused by malicious extensions. Changes: - Thread extensionId through the IPC boundary alongside extensionName - Inject IStorageService + IDialogService into MainThreadWorkspace - Add _resolveSuppress() trust gate with confirm dialog - Update vscode.d.ts docs to describe first-use consent behavior - Update test to verify extensionId is forwarded
| const veto = await handleVetos(vetos, error => this._logService.error(error)); | ||
| if (force) { | ||
| // force mode: onWillStop fired above so listeners could persist state, | ||
| // but we ignore veto results and stop unconditionally. | ||
| await this._doStopExtensionHosts(); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
In force mode, this still uses handleVetos(vetos, ...), which short-circuits as soon as any listener calls veto(true, ...) synchronously. That means other listeners’ long-running veto promises may not be awaited before extension hosts are stopped, despite the API/docs implying listeners can run long operations before stop. Consider a dedicated force path that fires onWillStop and then awaits settlement of all thenable veto values (logging errors) without short-circuiting, before stopping hosts unconditionally.
| const veto = await handleVetos(vetos, error => this._logService.error(error)); | |
| if (force) { | |
| // force mode: onWillStop fired above so listeners could persist state, | |
| // but we ignore veto results and stop unconditionally. | |
| await this._doStopExtensionHosts(); | |
| return true; | |
| } | |
| if (force) { | |
| // Force mode: onWillStop fired above so listeners can persist state. | |
| // We ignore veto results but still await settlement of all veto promises | |
| // (without short-circuiting) before stopping the extension hosts. | |
| const vetoPromises: Promise<unknown>[] = []; | |
| for (const value of vetos) { | |
| if (typeof value === 'boolean') { | |
| continue; | |
| } | |
| vetoPromises.push(value.then(undefined, error => this._logService.error(error))); | |
| } | |
| if (vetoPromises.length > 0) { | |
| await Promise.allSettled(vetoPromises); | |
| } | |
| await this._doStopExtensionHosts(); | |
| return true; | |
| } | |
| const veto = await handleVetos(vetos, error => this._logService.error(error)); |
| detail: localize( | ||
| 'suppressConfirmation.detail', | ||
| "This extension is requesting permission to add, remove, or replace workspace folders without showing the confirmation dialog. This enables automated workflows such as branch-per-chat session switching.\n\nYou can change this later in Settings." | ||
| ), | ||
| primaryButton: localize('suppressConfirmation.allow', "Allow"), | ||
| cancelButton: localize('suppressConfirmation.deny', "Don't Allow"), | ||
| }); |
There was a problem hiding this comment.
The consent dialog text says "You can change this later in Settings.", but this PR only persists a boolean in application storage and there doesn’t appear to be any Settings surface to manage/revoke this permission. Please either implement a discoverable way to change it (e.g. a setting/command/permissions UI) or adjust the dialog copy to avoid implying a settings toggle exists.
| "'{0}' wants to modify workspace folders silently", | ||
| extensionName |
There was a problem hiding this comment.
For a permission/consent prompt, relying only on extensionName (displayName/name) is spoofable/ambiguous. Since you already receive extensionId, consider including it (or a publisher-qualified label) in the dialog message/detail so users can clearly identify which extension is requesting silent workspace changes.
| "'{0}' wants to modify workspace folders silently", | |
| extensionName | |
| "'{0}' ({1}) wants to modify workspace folders silently", | |
| extensionName, | |
| extensionId |
Summary
Add a
suppressConfirmationoption toupdateWorkspaceFolders()that allows extensions to skip the workspace-transition confirmation dialog when programmatically modifying workspace folders.Closes #306495
Problem
When an extension calls
vscode.workspace.updateWorkspaceFolders()and the change triggers a workspace transition (e.g., single-folder to multi-folder, or replacing all folders), VS Code shows a confirmation dialog warning about extension host restarts. Extensions cannot suppress this dialog, even when they fully understand and expect the lifecycle implications.This blocks several automated workflows:
In all these cases, the confirmation dialog interrupts a programmatic flow where the extension has already committed to the operation.
Approach
New API Surface
Implementation Architecture
The option flows through the full call chain:
Key design decisions:
Named interface, not inline type:
IEnterWorkspaceOptionsis defined once inworkspaceEditing.tsand imported everywhere, following VS Code's convention for options that flow through service layers.forceparameter onstopExtensionHosts: Rather than having the workspace service call the protected_doStopExtensionHosts()directly (breaking encapsulation), this PR adds aforce?: booleanparameter to the publicstopExtensionHosts(reason, auto, force?)method. Whenforceistrue, the method bypasses the extension host veto chain and calls_doStopExtensionHosts()internally. This keeps the extension service's internal state management intact.Flat boolean across IPC: The protocol uses a flat
suppressConfirmation?: booleanrather than a nested options object, following the existing IPC convention for optional parameters.Overload detection: The
extHost.api.impl.tsbinding distinguishes betweenupdateWorkspaceFolders(0, 1, { uri })(folder) andupdateWorkspaceFolders(0, 1, { suppressConfirmation: true })(options) by checking for theuriproperty. This is safe becauseuriis a required property on workspace folder descriptors.Files Changed (11)
vscode.d.tsUpdateWorkspaceFoldersOptionsinterface + overloadextHost.api.impl.tsextHostWorkspace.tssuppressConfirmationextHost.protocol.tsmainThreadWorkspace.tsIEnterWorkspaceOptionsworkspaceEditing.tsIEnterWorkspaceOptionsinterface definitionabstractWorkspaceEditingService.tsupdateFoldersworkspaceEditingService.ts(electron)forcetostopExtensionHostsworkspaceEditingService.ts(browser)extensions.tsforceparam onstopExtensionHostsabstractExtensionService.tsforce=trueUse Cases
git copilot-quickstartand similar tools set up multi-folder workspaces as part of project initialization. The dialog interrupts an automated flow where the tool knows exactly what it's doing.Testing
Manual verification that:
updateWorkspaceFolderswithout options shows the dialog as before (backward compatible){ suppressConfirmation: true }skips the dialog and proceeds immediately{ uri }(folder) from{ suppressConfirmation }(options)