Skip to content

Commit d1f0211

Browse files
Desktop maintenance: unsafe base path warning (#6750)
Surface unsafe base path validation in the desktop maintenance view and add an installation-fix auto-refresh after successful tasks. <img width="1080" height="870" alt="image" src="https://github.com/user-attachments/assets/26fe61be-fed8-47c0-a921-604f0af018f8" /> ┆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)
1 parent cc42c29 commit d1f0211

File tree

5 files changed

+230
-4
lines changed

5 files changed

+230
-4
lines changed

apps/desktop-ui/src/constants/desktopMaintenanceTasks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export const DESKTOP_MAINTENANCE_TASKS: Readonly<MaintenanceTask>[] = [
1616
execute: async () => await electron.setBasePath(),
1717
name: 'Base path',
1818
shortDescription: 'Change the application base path.',
19-
errorDescription: 'Unable to open the base path. Please select a new one.',
19+
errorDescription:
20+
'The current base path is invalid or unsafe. Please select a new location.',
2021
description:
2122
'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.',
2223
isInstallationFix: true,

apps/desktop-ui/src/stores/maintenanceTaskStore.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
8585
const electron = electronAPI()
8686

8787
// Reactive state
88+
const lastUpdate = ref<InstallValidation | null>(null)
8889
const isRefreshing = ref(false)
8990
const isRunningTerminalCommand = computed(() =>
9091
tasks.value
@@ -97,6 +98,13 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
9798
.some((task) => getRunner(task)?.executing)
9899
)
99100

101+
const unsafeBasePath = computed(
102+
() => lastUpdate.value?.unsafeBasePath === true
103+
)
104+
const unsafeBasePathReason = computed(
105+
() => lastUpdate.value?.unsafeBasePathReason
106+
)
107+
100108
// Task list
101109
const tasks = ref(DESKTOP_MAINTENANCE_TASKS)
102110

@@ -123,6 +131,7 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
123131
* @param validationUpdate Update details passed in by electron
124132
*/
125133
const processUpdate = (validationUpdate: InstallValidation) => {
134+
lastUpdate.value = validationUpdate
126135
const update = validationUpdate as IndexedUpdate
127136
isRefreshing.value = true
128137

@@ -155,14 +164,20 @@ export const useMaintenanceTaskStore = defineStore('maintenanceTask', () => {
155164
}
156165

157166
const execute = async (task: MaintenanceTask) => {
158-
return getRunner(task).execute(task)
167+
const success = await getRunner(task).execute(task)
168+
if (success && task.isInstallationFix) {
169+
await refreshDesktopTasks()
170+
}
171+
return success
159172
}
160173

161174
return {
162175
tasks,
163176
isRefreshing,
164177
isRunningTerminalCommand,
165178
isRunningInstallationFix,
179+
unsafeBasePath,
180+
unsafeBasePathReason,
166181
execute,
167182
getRunner,
168183
processUpdate,
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// eslint-disable-next-line storybook/no-renderer-packages
2+
import type { Meta, StoryObj } from '@storybook/vue3'
3+
import { defineAsyncComponent } from 'vue'
4+
5+
type UnsafeReason = 'appInstallDir' | 'updaterCache' | 'oneDrive' | null
6+
type ValidationIssueState = 'OK' | 'warning' | 'error' | 'skipped'
7+
8+
type ValidationState = {
9+
inProgress: boolean
10+
installState: string
11+
basePath?: ValidationIssueState
12+
unsafeBasePath: boolean
13+
unsafeBasePathReason: UnsafeReason
14+
venvDirectory?: ValidationIssueState
15+
pythonInterpreter?: ValidationIssueState
16+
pythonPackages?: ValidationIssueState
17+
uv?: ValidationIssueState
18+
git?: ValidationIssueState
19+
vcRedist?: ValidationIssueState
20+
upgradePackages?: ValidationIssueState
21+
}
22+
23+
const validationState: ValidationState = {
24+
inProgress: false,
25+
installState: 'installed',
26+
basePath: 'OK',
27+
unsafeBasePath: false,
28+
unsafeBasePathReason: null,
29+
venvDirectory: 'OK',
30+
pythonInterpreter: 'OK',
31+
pythonPackages: 'OK',
32+
uv: 'OK',
33+
git: 'OK',
34+
vcRedist: 'OK',
35+
upgradePackages: 'OK'
36+
}
37+
38+
const createMockElectronAPI = () => {
39+
const logListeners: Array<(message: string) => void> = []
40+
41+
const getValidationUpdate = () => ({
42+
...validationState
43+
})
44+
45+
return {
46+
getPlatform: () => 'darwin',
47+
changeTheme: (_theme: unknown) => {},
48+
onLogMessage: (listener: (message: string) => void) => {
49+
logListeners.push(listener)
50+
},
51+
showContextMenu: (_options: unknown) => {},
52+
Events: {
53+
trackEvent: (_eventName: string, _data?: unknown) => {}
54+
},
55+
Validation: {
56+
onUpdate: (_callback: (update: unknown) => void) => {},
57+
async getStatus() {
58+
return getValidationUpdate()
59+
},
60+
async validateInstallation(callback: (update: unknown) => void) {
61+
callback(getValidationUpdate())
62+
},
63+
async complete() {
64+
// Only allow completion when the base path is safe
65+
return !validationState.unsafeBasePath
66+
},
67+
dispose: () => {}
68+
},
69+
setBasePath: () => Promise.resolve(true),
70+
reinstall: () => Promise.resolve(),
71+
uv: {
72+
installRequirements: () => Promise.resolve(),
73+
clearCache: () => Promise.resolve(),
74+
resetVenv: () => Promise.resolve()
75+
}
76+
}
77+
}
78+
79+
const ensureElectronAPI = () => {
80+
const globalWindow = window as unknown as { electronAPI?: unknown }
81+
if (!globalWindow.electronAPI) {
82+
globalWindow.electronAPI = createMockElectronAPI()
83+
}
84+
85+
return globalWindow.electronAPI
86+
}
87+
88+
const MaintenanceView = defineAsyncComponent(async () => {
89+
ensureElectronAPI()
90+
const module = await import('./MaintenanceView.vue')
91+
return module.default
92+
})
93+
94+
const meta: Meta<typeof MaintenanceView> = {
95+
title: 'Desktop/Views/MaintenanceView',
96+
component: MaintenanceView,
97+
parameters: {
98+
layout: 'fullscreen',
99+
backgrounds: {
100+
default: 'dark',
101+
values: [
102+
{ name: 'dark', value: '#0a0a0a' },
103+
{ name: 'neutral-900', value: '#171717' },
104+
{ name: 'neutral-950', value: '#0a0a0a' }
105+
]
106+
}
107+
}
108+
}
109+
110+
export default meta
111+
type Story = StoryObj<typeof meta>
112+
113+
export const Default: Story = {
114+
name: 'All tasks OK',
115+
render: () => ({
116+
components: { MaintenanceView },
117+
setup() {
118+
validationState.inProgress = false
119+
validationState.installState = 'installed'
120+
validationState.basePath = 'OK'
121+
validationState.unsafeBasePath = false
122+
validationState.unsafeBasePathReason = null
123+
validationState.venvDirectory = 'OK'
124+
validationState.pythonInterpreter = 'OK'
125+
validationState.pythonPackages = 'OK'
126+
validationState.uv = 'OK'
127+
validationState.git = 'OK'
128+
validationState.vcRedist = 'OK'
129+
validationState.upgradePackages = 'OK'
130+
ensureElectronAPI()
131+
return {}
132+
},
133+
template: '<MaintenanceView />'
134+
})
135+
}
136+
137+
export const UnsafeBasePathOneDrive: Story = {
138+
name: 'Unsafe base path (OneDrive)',
139+
render: () => ({
140+
components: { MaintenanceView },
141+
setup() {
142+
validationState.inProgress = false
143+
validationState.installState = 'installed'
144+
validationState.basePath = 'error'
145+
validationState.unsafeBasePath = true
146+
validationState.unsafeBasePathReason = 'oneDrive'
147+
validationState.venvDirectory = 'OK'
148+
validationState.pythonInterpreter = 'OK'
149+
validationState.pythonPackages = 'OK'
150+
validationState.uv = 'OK'
151+
validationState.git = 'OK'
152+
validationState.vcRedist = 'OK'
153+
validationState.upgradePackages = 'OK'
154+
ensureElectronAPI()
155+
return {}
156+
},
157+
template: '<MaintenanceView />'
158+
})
159+
}

apps/desktop-ui/src/views/MaintenanceView.vue

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,28 @@
4747
</div>
4848
</div>
4949

50+
<!-- Unsafe migration warning -->
51+
<div v-if="taskStore.unsafeBasePath" class="my-4">
52+
<p class="flex items-start gap-3 text-neutral-300">
53+
<Tag
54+
icon="pi pi-exclamation-triangle"
55+
severity="warn"
56+
:value="t('icon.exclamation-triangle')"
57+
/>
58+
<span>
59+
<strong class="block mb-1">
60+
{{ t('maintenance.unsafeMigration.title') }}
61+
</strong>
62+
<span class="block mb-1">
63+
{{ unsafeReasonText }}
64+
</span>
65+
<span class="block text-sm text-neutral-400">
66+
{{ t('maintenance.unsafeMigration.action') }}
67+
</span>
68+
</span>
69+
</p>
70+
</div>
71+
5072
<!-- Tasks -->
5173
<TaskListPanel
5274
class="border-neutral-700 border-solid border-x-0 border-y"
@@ -89,10 +111,10 @@
89111
import { PrimeIcons } from '@primevue/core/api'
90112
import Button from 'primevue/button'
91113
import SelectButton from 'primevue/selectbutton'
114+
import Tag from 'primevue/tag'
92115
import Toast from 'primevue/toast'
93116
import { useToast } from 'primevue/usetoast'
94-
import { computed, onMounted, onUnmounted, ref } from 'vue'
95-
import { watch } from 'vue'
117+
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
96118
97119
import RefreshButton from '@/components/common/RefreshButton.vue'
98120
import StatusTag from '@/components/maintenance/StatusTag.vue'
@@ -139,6 +161,27 @@ const filterOptions = ref([
139161
/** Filter binding; can be set to show all tasks, or only errors. */
140162
const filter = ref<MaintenanceFilter>(filterOptions.value[0])
141163
164+
const unsafeReasonText = computed(() => {
165+
const reason = taskStore.unsafeBasePathReason
166+
if (!reason) {
167+
return t('maintenance.unsafeMigration.generic')
168+
}
169+
170+
if (reason === 'appInstallDir') {
171+
return t('maintenance.unsafeMigration.appInstallDir')
172+
}
173+
174+
if (reason === 'updaterCache') {
175+
return t('maintenance.unsafeMigration.updaterCache')
176+
}
177+
178+
if (reason === 'oneDrive') {
179+
return t('maintenance.unsafeMigration.oneDrive')
180+
}
181+
182+
return t('maintenance.unsafeMigration.generic')
183+
})
184+
142185
/** If valid, leave the validation window. */
143186
const completeValidation = async () => {
144187
const isValid = await electron.Validation.complete()

src/locales/en/main.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,14 @@
15001500
"taskFailed": "Task failed to run.",
15011501
"cannotContinue": "Unable to continue - errors remain",
15021502
"defaultDescription": "An error occurred while running a maintenance task."
1503+
},
1504+
"unsafeMigration": {
1505+
"title": "Unsafe install location detected",
1506+
"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.",
1507+
"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.",
1508+
"updaterCache": "Your base path is inside the ComfyUI updater cache, which is cleared on each update. Choose a different location for your data.",
1509+
"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.",
1510+
"action": "Use the \"Base path\" maintenance task below to move ComfyUI to a safe location."
15031511
}
15041512
},
15051513
"missingModelsDialog": {

0 commit comments

Comments
 (0)