From 1d29296a6fca7cd16eb40b69e4de1b51f14f4f1e Mon Sep 17 00:00:00 2001 From: Towsty Date: Wed, 2 Sep 2026 08:59:04 -0500 Subject: [PATCH] Keep Output on the generating clip when another job is queued. Queued generate/extend no longer hijacks the live progress bar, and Generate Extension stays clickable so another extension can wait in the job queue. Co-authored-by: Cursor --- pages/index.vue | 110 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/pages/index.vue b/pages/index.vue index eb7c173..005b1bf 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -1290,6 +1290,9 @@
+

+ {{ studioWaitingCount }} waiting in the job queue — Output stays on the clip that is generating. +

@@ -1448,9 +1451,9 @@ :disabled="!extendPrompt.trim()" @click="generateExtension" > - {{ videoBusy ? 'Queue extension' : 'Generate Extension' }} + {{ watchingLiveVideo() || videoBusy ? 'Queue extension' : 'Generate Extension' }} -

The current job keeps running in Output. This extension waits in the job queue.

+

The current job keeps running in Output. This extension waits in the job queue.

@@ -2597,6 +2600,7 @@ const videoJobId = ref('') const editJobId = ref('') const currentClipId = ref('') const extendOpen = ref(false) +const extendClipId = ref('') const extendPrompt = ref('') const extendDuration = ref(5) const lastFrameUrl = ref('') @@ -2818,6 +2822,25 @@ let videoListenGen = 0 let editListenGen = 0 let activeDraftId = '' +function watchingLiveVideo() { + return Boolean(videoJobId.value) && !videoSettledUi && (videoBusy.value || Boolean(videoSource)) +} + +function watchingLiveEdit() { + return Boolean(editJobId.value) && !editSettledUi && (editBusy.value || Boolean(editSource)) +} + +function queuedOffLiveOutput( + started: { queued?: boolean; jobId?: string }, + kind: 'video' | 'edit' +) { + if (started.queued) return true + if (kind === 'edit') { + return watchingLiveEdit() && Boolean(started.jobId) && started.jobId !== editJobId.value + } + return watchingLiveVideo() && Boolean(started.jobId) && started.jobId !== videoJobId.value +} + function formatElapsed(ms: number) { const seconds = Math.floor(ms / 1000) const mm = String(Math.floor(seconds / 60)).padStart(2, '0') @@ -2875,7 +2898,7 @@ const liveStatusMessage = computed(() => { return idleInputStatus('edit') } if (videoBusy.value || videoUrl.value || lockedSave.value) return statusMessage.value - if (outputQueuedJob.value || videoAwaitingBeast.value) { + if (videoAwaitingBeast.value || outputQueuedJob.value) { return statusMessage.value || 'Queued. Waiting for Beast to be free.' } return idleInputStatus('video') @@ -5046,16 +5069,15 @@ async function refreshStudioQueue() { }>('/api/studio-queue').catch(() => ({ jobs: [] })) studioJobs.value = data.jobs || [] const running = studioJobs.value.find(job => job.status === 'running' && job.liveJobId) - const queuedMine = Boolean( - outputStudioJobId.value - && studioJobs.value.some(job => job.id === outputStudioJobId.value && (job.status === 'waiting' || job.status === 'held')) - ) - if (queuedMine && running && running.id !== outputStudioJobId.value) { + if (running?.liveJobId) videoAwaitingBeast.value = false + if (watchingLiveVideo() && running?.liveJobId && running.liveJobId !== videoJobId.value) { + return + } + if (watchingLiveEdit() && running?.liveJobId && running.liveJobId !== editJobId.value) { return } - if (running?.liveJobId) videoAwaitingBeast.value = false if (running?.kind === 'edit') { - if (running.liveJobId && running.liveJobId !== editJobId.value && !editBusy.value) { + if (running.liveJobId && running.liveJobId !== editJobId.value && !editBusy.value && !watchingLiveVideo()) { outputStudioJobId.value = running.id editJobId.value = running.liveJobId persistActiveJob('edit', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true) @@ -5066,7 +5088,7 @@ async function refreshStudioQueue() { } else if (running.id && running.liveJobId === editJobId.value) { outputStudioJobId.value = running.id } - } else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value) { + } else if (running?.liveJobId && running.liveJobId !== videoJobId.value && !videoBusy.value && !watchingLiveVideo()) { outputStudioJobId.value = running.id videoJobId.value = running.liveJobId persistActiveJob('video', running.liveJobId, running.hideThumbnail === true, running.folderLocked === true) @@ -5696,6 +5718,7 @@ function clipVideoUrl(clipId: string) { function closeExtend() { lastFrameGen += 1 extendOpen.value = false + extendClipId.value = '' lastFrameLoading.value = false lastFrameRevealed.value = false if (lastFrameObjectUrl) { @@ -5739,6 +5762,7 @@ async function openExtend(item?: LibraryClip) { return } currentClipId.value = id + extendClipId.value = id const clip = clips.value.find(entry => entry.id === id) if (clip) applyPromptRestore(clip.prompt, clip) extendPrompt.value = clip?.prompt || prompt.value @@ -5755,9 +5779,14 @@ async function openExtend(item?: LibraryClip) { } async function generateExtension() { - const id = clipIdFromPlayer() - if (!id || !extendPrompt.value.trim()) return + const id = extendClipId.value || clipIdFromPlayer() + if (!extendPrompt.value.trim()) return + if (!id) { + toast('Load a generated clip before extending it.') + return + } const hideOut = hideThumbnail.value + const keepLiveOutput = watchingLiveVideo() try { await ensureGenerationNotifyPermission() const started = await $fetch<{ @@ -5780,16 +5809,22 @@ async function generateExtension() { loraStack: extendLoraStack.value } }) - closeExtend() - if (started.queued) { + if (queuedOffLiveOutput(started, 'video')) { + if (watchingLiveVideo()) { + toast('Queued behind the current job. Output stays on the clip that is generating.') + await refreshStudioQueue() + return + } + toast('Queued behind the current job.') outputStudioJobId.value = started.studioJobId || '' outputFocus.value = 'video' videoAwaitingBeast.value = true statusMessage.value = 'Queued. Waiting for Beast to be free.' - toast('Queued behind the current job.') + closeExtend() await refreshStudioQueue() return } + closeExtend() statusBusy.value = false videoBusy.value = true watchingLibrary.value = false @@ -5816,12 +5851,14 @@ async function generateExtension() { void pollComfyHealth() void refreshStudioQueue() } catch (error: any) { - videoBusy.value = false - statusBusy.value = false - stopTimer('video') - clearActiveJob('video') const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start extension' - statusMessage.value = message + if (!keepLiveOutput) { + videoBusy.value = false + statusBusy.value = false + stopTimer('video') + clearActiveJob('video') + statusMessage.value = message + } toast(message) } } @@ -6982,6 +7019,7 @@ async function generate() { : extensionQueue.value.map(item => ({ prompt: item.prompt.trim(), duration: item.duration, ...persistLoraFields(item.loraStack || []) })) const hideOut = hideThumbnail.value const folderLockedPref = false + const keepLiveOutput = watchingLiveVideo() try { await ensureGenerationNotifyPermission() const body = new FormData() @@ -7059,15 +7097,23 @@ async function generate() { method: 'POST', body }) - if (started.queued) { + const queuedShotCount = 1 + queued.length + const queuedBehindToast = queuedShotCount > 1 + ? `Queued ${queuedShotCount} shots behind the current job.` + : 'Queued behind the current job.' + if (queuedOffLiveOutput(started, 'video')) { + if (watchingLiveVideo()) { + toast(queuedShotCount > 1 + ? `Queued ${queuedShotCount} shots. Output stays on the clip that is generating.` + : 'Queued behind the current job. Output stays on the clip that is generating.') + await refreshStudioQueue() + return + } outputStudioJobId.value = started.studioJobId || '' outputFocus.value = 'video' videoAwaitingBeast.value = true statusMessage.value = 'Queued. Waiting for Beast to be free.' - const n = 1 + queued.length - toast(n > 1 - ? `Queued ${n} shots behind the current job.` - : 'Queued behind the current job.') + toast(queuedBehindToast) await refreshStudioQueue() return } @@ -7107,12 +7153,14 @@ async function generate() { void pollComfyHealth() void refreshStudioQueue() } catch (error: any) { - videoBusy.value = false - statusBusy.value = false - stopTimer('video') - clearActiveJob('video') const message = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Failed to start generation' - statusMessage.value = message + if (!keepLiveOutput) { + videoBusy.value = false + statusBusy.value = false + stopTimer('video') + clearActiveJob('video') + statusMessage.value = message + } toast(message) } }