Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/core/config/importExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file src/core/config/tests/importExport.spec.ts still mocks vscode.window.showOpenDialog and vscode.window.showSaveDialog directly. Should the tests be updated to mock the new safe wrapper functions instead?


import { globalSettingsSchema } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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")),
})
Expand Down
9 changes: 5 additions & 4 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions src/integrations/misc/export-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)),
})
Expand Down
5 changes: 3 additions & 2 deletions src/integrations/misc/image-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,(.+)$/)
Expand Down Expand Up @@ -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": ["*"],
Expand Down
46 changes: 46 additions & 0 deletions src/utils/safeDialogs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as vscode from "vscode"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new utility module should have unit tests to verify the non-blocking behavior and error handling. Could we add tests to ensure:

  1. The functions are called with the correct options
  2. Results are properly returned
  3. Errors are handled gracefully
  4. The deferred execution via setImmediate works as expected


/**
* 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<vscode.Uri | undefined> {
// Use setImmediate to defer the dialog call to the next iteration of the event loop
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be helpful to add a comment explaining why setImmediate specifically prevents the UI freeze? This would help future maintainers understand the mechanism behind this fix.

For example: 'setImmediate schedules the callback to execute after the current event loop completes, allowing the UI thread to process pending events and remain responsive.'

// This prevents the UI thread from being blocked
return new Promise<vscode.Uri | undefined>((resolve) => {
setImmediate(async () => {
try {
const result = await vscode.window.showSaveDialog(options)
resolve(result)
} catch (error) {
console.error("Error showing save dialog:", error)
resolve(undefined)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is resolving to undefined on error intentional? The error is logged, but callers can't distinguish between user cancellation and an actual error. Would it be better to re-throw the error after logging, or is this defensive behavior by design?

}
})
})
}

/**
* 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<vscode.Uri[] | undefined> {
// Use setImmediate to defer the dialog call to the next iteration of the event loop
return new Promise<vscode.Uri[] | undefined>((resolve) => {
setImmediate(async () => {
try {
const result = await vscode.window.showOpenDialog(options)
resolve(result)
} catch (error) {
console.error("Error showing open dialog:", error)
resolve(undefined)
}
})
})
}
Loading