Stop idle Comfy after 30 minutes and add a Wake GPU poke.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-29 17:59:18 -05:00
co-authored by Cursor
parent cdc69d2bd6
commit ad1f4f905a
8 changed files with 283 additions and 26 deletions
+71 -16
View File
@@ -10,14 +10,27 @@
</div>
</div>
<div class="flex items-center gap-3 text-sm">
<span
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
:class="rigBadge.className"
:title="rigBadge.title"
>
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
{{ rigBadge.label }}
</span>
<div class="flex items-center gap-2">
<span
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
:class="rigBadge.className"
:title="rigBadge.title"
>
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
{{ rigBadge.label }}
</span>
<button
v-if="showWakeGpu"
type="button"
class="rounded-full border px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white disabled:cursor-wait disabled:opacity-80"
:class="gpuWaking ? 'border-amber-300/40 text-amber-100' : 'border-white/10'"
:disabled="gpuWaking"
:title="gpuWakeError || (gpuWaking ? 'Starting Comfy Desktop on the GPU' : 'Poke / Wake GPU — start Comfy before generating')"
@click="pokeGpu"
>
{{ gpuWaking ? 'Starting GPU…' : 'Wake GPU' }}
</button>
</div>
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">
Settings
@@ -2358,6 +2371,9 @@ const playerSize = ref({ width: 1344, height: 768 })
const toasts = ref<{ id: string; message: string }[]>([])
const comfyOk = ref(false)
const comfyBusy = ref(false)
const comfyAsleep = ref(false)
const gpuWaking = ref(false)
const gpuWakeError = ref('')
const userName = ref('')
const authEnabled = ref(false)
const authMode = ref('')
@@ -2574,13 +2590,24 @@ const liveMaxStep = computed(() => (
const rigBusy = computed(() => (
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
))
const showWakeGpu = computed(() => gpuWaking.value || (!comfyOk.value && !imageComfyOk.value))
const rigBadge = computed(() => {
if (gpuWaking.value) {
return {
label: 'Starting…',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Starting Comfy Desktop on the GPU'
}
}
if (!comfyOk.value && !imageComfyOk.value) {
return {
label: 'Offline',
className: 'border-red-400/30 text-red-300',
dot: 'bg-red-400',
title: 'Desktop GPU is unreachable'
title: comfyAsleep.value
? 'Desktop GPU is asleep. Click Wake GPU or generate to start Comfy.'
: 'Desktop GPU is unreachable'
}
}
if (rigBusy.value) {
@@ -2696,7 +2723,7 @@ const recommendHint = computed(() => {
const editBlocked = computed(() => {
if (!file.value || !prompt.value.trim() || !folderId.value) return true
if (editPassQueue.value.some(item => !item.prompt.trim())) return true
if (!comfyOk.value && !imageComfyOk.value) return true
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return true
return false
})
const editBlockReason = computed(() => {
@@ -2705,7 +2732,7 @@ const editBlockReason = computed(() => {
if (!folderId.value) return 'Choose a library folder before editing.'
if (editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return 'Desktop GPU is offline. Click Wake GPU first.'
return ''
})
const stillBHint = computed(() => {
@@ -2719,7 +2746,7 @@ const editV2Blocked = computed(() => {
if (v2Mode.value !== 'generate' && !file.value) return true
if (v2Mode.value === 'compose' && !editRefFile.value) return true
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
if (!comfyOk.value && !imageComfyOk.value) return true
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return true
return false
})
const editV2BlockReason = computed(() => {
@@ -2729,7 +2756,7 @@ const editV2BlockReason = computed(() => {
if (!prompt.value.trim()) return 'Write a prompt first.'
if (!folderId.value) return 'Choose a library folder before generating.'
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
if (!comfyOk.value && !imageComfyOk.value && !gpuWaking.value) return 'Desktop GPU is offline. Click Wake GPU first.'
return ''
})
const editV2SubmitLabel = computed(() => {
@@ -3206,7 +3233,7 @@ function scheduleLibraryRefresh() {
let healthFailStreak = 0
function applyComfyHealth(health: {
comfy?: { ok?: boolean; busy?: boolean }
comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean }
imageComfy?: {
ok?: boolean
busy?: boolean
@@ -3219,7 +3246,11 @@ function applyComfyHealth(health: {
const nextComfyOk = Boolean(health?.comfy?.ok)
const nextImageOk = Boolean(health?.imageComfy?.ok)
const reachable = nextComfyOk || nextImageOk
if (reachable) healthFailStreak = 0
if (reachable) {
healthFailStreak = 0
gpuWaking.value = false
gpuWakeError.value = ''
}
else healthFailStreak += 1
const keepLast = !reachable && (healthFailStreak < 3 || videoBusy.value || editBusy.value)
if (!keepLast) {
@@ -3227,6 +3258,7 @@ function applyComfyHealth(health: {
imageComfyOk.value = nextImageOk
imageBeastOk.value = Boolean(health?.imageComfy?.beast?.ok)
imageSidecarOk.value = Boolean(health?.imageComfy?.sidecar?.ok)
comfyAsleep.value = Boolean(health?.comfy?.asleep) && !reachable
}
comfyBusy.value = Boolean((keepLast ? comfyOk.value : nextComfyOk) && health?.comfy?.busy)
imageComfyConfigured.value = Boolean(health?.imageComfy?.configured)
@@ -3246,10 +3278,33 @@ async function pollLoras() {
}
async function pollComfyHealth() {
const health = await $fetch<{ comfy?: { ok?: boolean; busy?: boolean } }>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } }))
const health = await $fetch<{ comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean } }>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } }))
applyComfyHealth(health)
}
async function pokeGpu() {
if (gpuWaking.value) return
gpuWaking.value = true
gpuWakeError.value = ''
try {
await $fetch('/api/comfy/wake', { method: 'POST' })
const deadline = Date.now() + 180_000
while (Date.now() < deadline) {
await pollComfyHealth()
if (comfyOk.value || imageComfyOk.value) {
gpuWaking.value = false
return
}
await new Promise(resolve => setTimeout(resolve, 3000))
}
gpuWakeError.value = 'Started Comfy but it is not answering yet. If the instance picker is showing, open ComfyUI (1).'
} catch (error: any) {
gpuWakeError.value = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Could not wake the GPU.'
} finally {
gpuWaking.value = false
}
}
onMounted(async () => {
try {
withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false'