From 5f85198db705ffa49afa66f65baedb9c0382cc84 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Wed, 19 Nov 2025 18:13:32 -0800 Subject: [PATCH] Desktop maintenance: unsafe base path warning (#6750) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surface unsafe base path validation in the desktop maintenance view and add an installation-fix auto-refresh after successful tasks. image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6750-Desktop-maintenance-unsafe-base-path-warning-2b06d73d36508147aeb4d19d02bbf0f0) by [Unito](https://www.unito.io) --- .../src/constants/desktopMaintenanceTasks.ts | 3 +- .../src/stores/maintenanceTaskStore.ts | 17 +- .../src/views/MaintenanceView.stories.ts | 159 ++++++++++++++++++ apps/desktop-ui/src/views/MaintenanceView.vue | 47 +++++- src/locales/en/main.json | 8 + 5 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 apps/desktop-ui/src/views/MaintenanceView.stories.ts diff --git a/apps/desktop-ui/src/constants/desktopMaintenanceTasks.ts b/apps/desktop-ui/src/constants/desktopMaintenanceTasks.ts index 082a62045..9f81ab394 100644 --- a/apps/desktop-ui/src/constants/desktopMaintenanceTasks.ts +++ b/apps/desktop-ui/src/constants/desktopMaintenanceTasks.ts @@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly[] = [ execute: async () => await electron.setBasePath(), name: 'Base path', shortDescription: 'Change the application base path.', - errorDescription: 'Unable to open the base path. Please select a new one.', + errorDescription: + 'The current base path is invalid or unsafe. Please select a new location.', description: 'The base path is the default location where ComfyUI stores data. It is the location for the python environment, and may also contain models, custom nodes, and other extensions.', isInstallationFix: true, diff --git a/apps/desktop-ui/src/stores/maintenanceTaskStore.ts b/apps/desktop-ui/src/stores/maintenanceTaskStore.ts index 7ce4811cf..d3dfca18c 100644 --- a/apps/desktop-ui/src/stores/maintenanceTaskStore.ts +++ b/apps/desktop-ui/src/stores/maintenanceTaskStore.ts @@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => { const electron = electronAPI() // Reactive state + const lastUpdate = ref(null) const isRefreshing = ref(false) const isRunningTerminalCommand = computed(() => tasks.value @@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => { .some((task) => getRunner(task)?.executing) ) + const unsafeBasePath = computed( + () => lastUpdate.value?.unsafeBasePath === true + ) + const unsafeBasePathReason = computed( + () => lastUpdate.value?.unsafeBasePathReason + ) + // Task list const tasks = ref(DESKTOP_MAINTENANCE_TASKS) @@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => { * @param validationUpdate Update details passed in by electron */ const processUpdate = (validationUpdate: InstallValidation) => { + lastUpdate.value = validationUpdate const update = validationUpdate as IndexedUpdate isRefreshing.value = true @@ -155,7 +164,11 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => { } const execute = async (task: MaintenanceTask) => { - return getRunner(task).execute(task) + const success = await getRunner(task).execute(task) + if (success && task.isInstallationFix) { + await refreshDesktopTasks() + } + return success } return { @@ -163,6 +176,8 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => { isRefreshing, isRunningTerminalCommand, isRunningInstallationFix, + unsafeBasePath, + unsafeBasePathReason, execute, getRunner, processUpdate, diff --git a/apps/desktop-ui/src/views/MaintenanceView.stories.ts b/apps/desktop-ui/src/views/MaintenanceView.stories.ts new file mode 100644 index 000000000..5dee7106b --- /dev/null +++ b/apps/desktop-ui/src/views/MaintenanceView.stories.ts @@ -0,0 +1,159 @@ +// eslint-disable-next-line storybook/no-renderer-packages +import type { Meta, StoryObj } from '@storybook/vue3' +import { defineAsyncComponent } from 'vue' + +type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null +type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped' + +type ValidationState = { + inProgress: boolean + installState: string + basePath?: ValidationIssueState + unsafeBasePath: boolean + unsafeBasePathReason: UnsafeReason + venvDirectory?: ValidationIssueState + pythonInterpreter?: ValidationIssueState + pythonPackages?: ValidationIssueState + uv?: ValidationIssueState + git?: ValidationIssueState + vcRedist?: ValidationIssueState + upgradePackages?: ValidationIssueState +} + +const validationState: ValidationState = { + inProgress: false, + installState: 'installed', + basePath: 'OK', + unsafeBasePath: false, + unsafeBasePathReason: null, + venvDirectory: 'OK', + pythonInterpreter: 'OK', + pythonPackages: 'OK', + uv: 'OK', + git: 'OK', + vcRedist: 'OK', + upgradePackages: 'OK' +} + +const createMockElectronAPI = () => { + const logListeners: Array<(message: string) => void> = [] + + const getValidationUpdate = () => ({ + ...validationState + }) + + return { + getPlatform: () => 'darwin', + changeTheme: (_theme: unknown) => {}, + onLogMessage: (listener: (message: string) => void) => { + logListeners.push(listener) + }, + showContextMenu: (_options: unknown) => {}, + Events: { + trackEvent: (_eventName: string, _data?: unknown) => {} + }, + Validation: { + onUpdate: (_callback: (update: unknown) => void) => {}, + async getStatus() { + return getValidationUpdate() + }, + async validateInstallation(callback: (update: unknown) => void) { + callback(getValidationUpdate()) + }, + async complete() { + // Only allow completion when the base path is safe + return !validationState.unsafeBasePath + }, + dispose: () => {} + }, + setBasePath: () => Promise.resolve(true), + reinstall: () => Promise.resolve(), + uv: { + installRequirements: () => Promise.resolve(), + clearCache: () => Promise.resolve(), + resetVenv: () => Promise.resolve() + } + } +} + +const ensureElectronAPI = () => { + const globalWindow = window as unknown as { electronAPI?: unknown } + if (!globalWindow.electronAPI) { + globalWindow.electronAPI = createMockElectronAPI() + } + + return globalWindow.electronAPI +} + +const MaintenanceView = defineAsyncComponent(async () => { + ensureElectronAPI() + const module = await import('./MaintenanceView.vue') + return module.default +}) + +const meta: Meta = { + title: 'Desktop/Views/MaintenanceView', + component: MaintenanceView, + parameters: { + layout: 'fullscreen', + backgrounds: { + default: 'dark', + values: [ + { name: 'dark', value: '#0a0a0a' }, + { name: 'neutral-900', value: '#171717' }, + { name: 'neutral-950', value: '#0a0a0a' } + ] + } + } +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + name: 'All tasks OK', + render: () => ({ + components: { MaintenanceView }, + setup() { + validationState.inProgress = false + validationState.installState = 'installed' + validationState.basePath = 'OK' + validationState.unsafeBasePath = false + validationState.unsafeBasePathReason = null + validationState.venvDirectory = 'OK' + validationState.pythonInterpreter = 'OK' + validationState.pythonPackages = 'OK' + validationState.uv = 'OK' + validationState.git = 'OK' + validationState.vcRedist = 'OK' + validationState.upgradePackages = 'OK' + ensureElectronAPI() + return {} + }, + template: '' + }) +} + +export const UnsafeBasePathOneDrive: Story = { + name: 'Unsafe base path (OneDrive)', + render: () => ({ + components: { MaintenanceView }, + setup() { + validationState.inProgress = false + validationState.installState = 'installed' + validationState.basePath = 'error' + validationState.unsafeBasePath = true + validationState.unsafeBasePathReason = 'oneDrive' + validationState.venvDirectory = 'OK' + validationState.pythonInterpreter = 'OK' + validationState.pythonPackages = 'OK' + validationState.uv = 'OK' + validationState.git = 'OK' + validationState.vcRedist = 'OK' + validationState.upgradePackages = 'OK' + ensureElectronAPI() + return {} + }, + template: '' + }) +} diff --git a/apps/desktop-ui/src/views/MaintenanceView.vue b/apps/desktop-ui/src/views/MaintenanceView.vue index dbe1b269e..433dda54d 100644 --- a/apps/desktop-ui/src/views/MaintenanceView.vue +++ b/apps/desktop-ui/src/views/MaintenanceView.vue @@ -47,6 +47,28 @@ + +
+

+ + + + {{ t('maintenance.unsafeMigration.title') }} + + + {{ unsafeReasonText }} + + + {{ t('maintenance.unsafeMigration.action') }} + + +

+
+ (filterOptions.value[0]) +const unsafeReasonText = computed(() => { + const reason = taskStore.unsafeBasePathReason + if (!reason) { + return t('maintenance.unsafeMigration.generic') + } + + if (reason === 'appInstallDir') { + return t('maintenance.unsafeMigration.appInstallDir') + } + + if (reason === 'updaterCache') { + return t('maintenance.unsafeMigration.updaterCache') + } + + if (reason === 'oneDrive') { + return t('maintenance.unsafeMigration.oneDrive') + } + + return t('maintenance.unsafeMigration.generic') +}) + /** If valid, leave the validation window. */ const completeValidation = async () => { const isValid = await electron.Validation.complete() diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 6e53b44bc..4a84af6b6 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1345,6 +1345,14 @@ "taskFailed": "Task failed to run.", "cannotContinue": "Unable to continue - errors remain", "defaultDescription": "An error occurred while running a maintenance task." + }, + "unsafeMigration": { + "title": "Unsafe install location detected", + "generic": "Your current ComfyUI base path is in a location that may be deleted or modified during updates. To avoid data loss, move it to a safe folder.", + "appInstallDir": "Your base path is inside the ComfyUI Desktop application bundle. This folder may be deleted or overwritten during updates. Choose a directory outside the install folder, such as Documents/ComfyUI.", + "updaterCache": "Your base path is inside the ComfyUI updater cache, which is cleared on each update. Choose a different location for your data.", + "oneDrive": "Your base path is on OneDrive, which can cause sync issues and accidental data loss. Choose a local folder that is not managed by OneDrive.", + "action": "Use the \"Base path\" maintenance task below to move ComfyUI to a safe location." } }, "missingModelsDialog": {