Skip to content
Open
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
2 changes: 2 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@
"updateConfirmMessage": "Update from {installed} to {latest}?\n\nLocal changes will be stashed and a backup branch created.",
"updateConfirmMessageLatest": "Update from {installed} to latest on master ({latest})?\n\nThis will pull the newest commits from the master branch. Local changes will be stashed and a backup branch created.",
"updateConfirmMessageDowngrade": "Roll back from {installed} to stable release {latest}?\n\nYour install is currently ahead of the latest stable release. This will check out the stable tag, removing any newer commits. Local changes will be stashed and a backup branch created.",
"updateRestartNotice": "ComfyUI is currently running and will be restarted to apply this update.",
"updatingTitle": "Updating to {version}",
"updatePrepare": "Prepare",
"updatePrepareSnapshot": "Saving snapshot of current state…",
Expand Down Expand Up @@ -831,6 +832,7 @@
"stopRequired": "This installation is currently running and must be stopped first.",
"stopRequiredConfirm": "This installation is currently running. Would you like to stop it to continue?",
"stopRunning": "Stop Running",
"stopTimeout": "ComfyUI is still shutting down. Please wait a moment and try again.",
"folderPermissionDenied": "macOS denied access to \"{path}\". Please open System Settings → Privacy & Security → Files and Folders, grant access to ComfyUI Desktop 2.0, then retry."
},

Expand Down
5 changes: 5 additions & 0 deletions src/renderer/src/composables/useDeepLinkRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,16 @@ export function useDeepLinkRouter(opts: DeepLinkRouterOpts): void {
await opts.bootstrapReady
const inst = installationStore.getById(id)
if (!inst) return
// Issue #557 — pill click drives straight to the update
// confirm dialog. The Update tab still opens behind the
// dialog so a cancel returns the user to the surface that
// shows release notes / channel info.
await opts.openOverlay({
kind: 'settings',
installation: inst,
initialTab: 'comfy',
initialDetailTab: 'update',
autoAction: 'update-comfyui',
})
return
}
Expand Down
7 changes: 5 additions & 2 deletions src/renderer/src/panel/PanelApp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ vi.mock('../views/DetailModal.vue', () => ({
// parent owns the close behaviour.
props: ['installation', 'initialTab', 'autoAction'],
template:
'<div data-testid="detail-modal" :data-installation-id="installation?.id" />',
'<div data-testid="detail-modal" :data-installation-id="installation?.id" :data-auto-action="autoAction ?? \'\'" />',
},
}))
vi.mock('../views/ProgressModal.vue', () => ({
Expand Down Expand Up @@ -722,7 +722,9 @@ describe('PanelApp', () => {
// `kind: 'install-update'` and the host's installationId. The
// panel renderer routes that into a Tier 1 manage overlay with
// initialTab='update' so the DetailModal opens directly on the
// update tab.
// update tab. Issue #557: it also pre-arms the `update-comfyui`
// auto-action so the confirm dialog fires immediately instead of
// making the user click "Update Now" inside the tab.
const wrapper = mountPanel()
await flushPromises()
expect(wrapper.find('[data-testid="detail-modal"]').exists()).toBe(false)
Expand All @@ -734,6 +736,7 @@ describe('PanelApp', () => {
const detail = wrapper.find('[data-testid="detail-modal"]')
expect(detail.exists()).toBe(true)
expect(detail.attributes('data-installation-id')).toBe('test-id')
expect(detail.attributes('data-auto-action')).toBe('update-comfyui')
})

it('shows the "Desktop Update Ready" confirm modal when a restart-prompt event arrives, and installs on confirm', async () => {
Expand Down
79 changes: 66 additions & 13 deletions src/renderer/src/views/DetailModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,36 @@ async function runAction(action: ActionDef, btn: HTMLButtonElement | null): Prom

// Pre-flight: check if the installation is busy or running
// Skip for migrate-to-standalone — the useMigrateAction composable handles its own guard
if (action.id !== 'migrate-to-standalone' && REQUIRES_STOPPED.has(action.id)) {
// Skip for update-comfyui — the smooth update flow folds the
// stop-required prompt into the action's own confirm dialog and
// chains stopComfyUI → update → launch in `apiCall` (issue #557).
if (
action.id !== 'migrate-to-standalone' &&
action.id !== 'update-comfyui' &&
REQUIRES_STOPPED.has(action.id)
) {
if (!await actionGuard.checkBeforeAction(props.installation.id, action.label)) return
}

let mutableAction = { ...action }

// Smooth update flow — when the install is running, the post-update
// auto-launch is implied by the action; tell the user that in the
// single confirm dialog rather than as a separate stop-required
// prompt. The chained stop+launch lives in the showProgress branch.
const updateWillRestart =
mutableAction.id === 'update-comfyui' && sessionStore.isRunning(props.installation.id)
if (updateWillRestart && mutableAction.confirm) {
const baseMsg = mutableAction.confirm.message ?? ''
mutableAction = {
...mutableAction,
confirm: {
...mutableAction.confirm,
message: `${t('standalone.updateRestartNotice')}\n\n${baseMsg}`,
},
}
}

// fieldSelects chain
if (mutableAction.fieldSelects) {
const selections: Record<string, FieldOption> = {}
Expand Down Expand Up @@ -512,24 +536,53 @@ async function runAction(action: ActionDef, btn: HTMLButtonElement | null): Prom
// continuous "Restarting ComfyUI" view rather than two flashes.
// The action's confirm dialog has already run above.
const isRestart = mutableAction.id === 'restart'
// Issue #557 — smooth update flow: when the install is running,
// chain stopComfyUI → update → launch in the same ProgressModal
// so the user gets a single uninterrupted view instead of having
// to click Done and then Start ComfyUI afterwards.
const isUpdateAndRestart = mutableAction.id === 'update-comfyui' && updateWillRestart
// Returns true once the session has actually left the running
// state, false if the 30s deadline expired with the session still
// running. Callers must short-circuit on false — the backend
// enforces REQUIRES_STOPPED and would reject the chained call
// with a vague {ok:false, running:true}, surfacing as a generic
// failure banner that doesn't tell the user what happened.
const waitForStopped = async (): Promise<boolean> => {
const deadline = Date.now() + 30_000
while (sessionStore.isRunning(instId) && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 100))
}
return !sessionStore.isRunning(instId)
}
const stopTimeoutResult = (): ActionResult => ({
ok: false,
message: t('errors.stopTimeout'),
})
const apiCall = isRestart
? async () => {
await window.api.stopComfyUI(instId)
// Wait for the session to actually leave the running state
// before kicking off the new launch. The session store is
// updated by main via 'session-status' broadcasts.
const deadline = Date.now() + 10_000
while (sessionStore.isRunning(instId) && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 100))
}
if (!(await waitForStopped())) return stopTimeoutResult()
return window.api.runAction(instId, 'launch')
}
: isUpdateAndRestart
? async () => {
await window.api.stopComfyUI(instId)
if (!(await waitForStopped())) return stopTimeoutResult()
const updateResult = await window.api.runAction(
instId,
mutableAction.id,
mutableAction.data ? toRaw(mutableAction.data) : undefined,
)
if (!updateResult.ok || updateResult.cancelled) return updateResult
return window.api.runAction(instId, 'launch')
}
: () => window.api.runAction(instId, mutableAction.id, mutableAction.data ? toRaw(mutableAction.data) : undefined)
// Tag launch / restart so PanelApp's handleShowProgress installs
// the chooser-host close-on-instance-started subscription. Without
// this, launches kicked off from this Tier-1 modal would leave the
// dashboard window open next to the new comfy window.
const triggersInstanceStart = mutableAction.id === 'launch' || isRestart
// Tag launch / restart / update-and-restart so PanelApp's
// handleShowProgress installs the chooser-host close-on-instance-
// started subscription. Without this, launches kicked off from
// this Tier-1 modal would leave the dashboard window open next to
// the new comfy window.
const triggersInstanceStart = mutableAction.id === 'launch' || isRestart || isUpdateAndRestart
emit('show-progress', {
installationId: instId,
title,
Expand Down
Loading