From c101ed9507317d5111aeb47d640f8e820df6983d Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 9 Aug 2025 13:38:15 +0000 Subject: [PATCH] fix: prevent UI freeze when showing save dialogs - Created safeDialogs utility with non-blocking wrappers for showSaveDialog and showOpenDialog - Updated all save/open dialog calls to use the safe wrappers - Uses setImmediate to defer dialog calls to next event loop iteration - Prevents VSCode UI from freezing on macOS with certain configurations Fixes #6870 --- src/core/config/importExport.ts | 5 ++- src/core/webview/webviewMessageHandler.ts | 9 +++-- src/integrations/misc/export-markdown.ts | 5 ++- src/integrations/misc/image-handler.ts | 5 ++- src/utils/safeDialogs.ts | 46 +++++++++++++++++++++++ 5 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 src/utils/safeDialogs.ts diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index c3d6f9c215..1ce4f232ce 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -5,6 +5,7 @@ import fs from "fs/promises" import * as vscode from "vscode" import { z, ZodError } from "zod" +import { showSaveDialogSafe, showOpenDialogSafe } from "../../utils/safeDialogs" import { globalSettingsSchema } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -109,7 +110,7 @@ export async function importSettingsFromPath( * @returns Promise resolving to import result */ export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => { - const uris = await vscode.window.showOpenDialog({ + const uris = await showOpenDialogSafe({ filters: { JSON: ["json"] }, canSelectMany: false, }) @@ -143,7 +144,7 @@ export const importSettingsFromFile = async ( } export const exportSettings = async ({ providerSettingsManager, contextProxy }: ExportOptions) => { - const uri = await vscode.window.showSaveDialog({ + const uri = await showSaveDialogSafe({ filters: { JSON: ["json"] }, defaultUri: vscode.Uri.file(path.join(os.homedir(), "Documents", "roo-code-settings.json")), }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e2c6d6a475..2f4de9ddc0 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -5,6 +5,7 @@ import * as fs from "fs/promises" import pWaitFor from "p-wait-for" import * as vscode from "vscode" import * as yaml from "yaml" +import { showSaveDialogSafe, showOpenDialogSafe } from "../../utils/safeDialogs" import { type Language, @@ -1791,8 +1792,8 @@ export const webviewMessageHandler = async ( } } - // Show save dialog - const saveUri = await vscode.window.showSaveDialog({ + // Show save dialog using safe wrapper to prevent UI freezing + const saveUri = await showSaveDialogSafe({ defaultUri, filters: { "YAML files": ["yaml", "yml"], @@ -1866,8 +1867,8 @@ export const webviewMessageHandler = async ( } } - // Show file picker to select YAML file - const fileUri = await vscode.window.showOpenDialog({ + // Show file picker to select YAML file using safe wrapper to prevent UI freezing + const fileUri = await showOpenDialogSafe({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, diff --git a/src/integrations/misc/export-markdown.ts b/src/integrations/misc/export-markdown.ts index 2d493ce50c..29486ec88a 100644 --- a/src/integrations/misc/export-markdown.ts +++ b/src/integrations/misc/export-markdown.ts @@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import os from "os" import * as path from "path" import * as vscode from "vscode" +import { showSaveDialogSafe } from "../../utils/safeDialogs" export async function downloadTask(dateTs: number, conversationHistory: Anthropic.MessageParam[]) { // File name @@ -28,8 +29,8 @@ export async function downloadTask(dateTs: number, conversationHistory: Anthropi }) .join("---\n\n") - // Prompt user for save location - const saveUri = await vscode.window.showSaveDialog({ + // Prompt user for save location using safe wrapper to prevent UI freezing + const saveUri = await showSaveDialogSafe({ filters: { Markdown: ["md"] }, defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", fileName)), }) diff --git a/src/integrations/misc/image-handler.ts b/src/integrations/misc/image-handler.ts index 4cd7585df4..0271420f53 100644 --- a/src/integrations/misc/image-handler.ts +++ b/src/integrations/misc/image-handler.ts @@ -3,6 +3,7 @@ import * as os from "os" import * as vscode from "vscode" import { getWorkspacePath } from "../../utils/path" import { t } from "../../i18n" +import { showSaveDialogSafe } from "../../utils/safeDialogs" export async function openImage(dataUri: string, options?: { values?: { action?: string } }) { const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) @@ -67,8 +68,8 @@ export async function saveImage(dataUri: string) { const defaultFileName = `mermaid_diagram_${Date.now()}.${format}` const defaultUri = vscode.Uri.file(path.join(defaultPath, defaultFileName)) - // Show save dialog - const saveUri = await vscode.window.showSaveDialog({ + // Show save dialog using safe wrapper to prevent UI freezing + const saveUri = await showSaveDialogSafe({ filters: { Images: [format], "All Files": ["*"], diff --git a/src/utils/safeDialogs.ts b/src/utils/safeDialogs.ts new file mode 100644 index 0000000000..45d1ce1096 --- /dev/null +++ b/src/utils/safeDialogs.ts @@ -0,0 +1,46 @@ +import * as vscode from "vscode" + +/** + * Wraps vscode.window.showSaveDialog in a non-blocking way to prevent UI freezing + * This addresses the issue where the save dialog can cause the entire VSCode UI to freeze + * on certain systems (particularly macOS with specific configurations). + * + * @param options - The save dialog options + * @returns Promise that resolves to the selected URI or undefined if cancelled + */ +export async function showSaveDialogSafe(options: vscode.SaveDialogOptions): Promise { + // Use setImmediate to defer the dialog call to the next iteration of the event loop + // This prevents the UI thread from being blocked + return new Promise((resolve) => { + setImmediate(async () => { + try { + const result = await vscode.window.showSaveDialog(options) + resolve(result) + } catch (error) { + console.error("Error showing save dialog:", error) + resolve(undefined) + } + }) + }) +} + +/** + * Wraps vscode.window.showOpenDialog in a non-blocking way to prevent UI freezing + * + * @param options - The open dialog options + * @returns Promise that resolves to the selected URIs or undefined if cancelled + */ +export async function showOpenDialogSafe(options: vscode.OpenDialogOptions): Promise { + // Use setImmediate to defer the dialog call to the next iteration of the event loop + return new Promise((resolve) => { + setImmediate(async () => { + try { + const result = await vscode.window.showOpenDialog(options) + resolve(result) + } catch (error) { + console.error("Error showing open dialog:", error) + resolve(undefined) + } + }) + }) +}