Pause only the current shot batch so a new Generate can run when Beast is idle, without auto-resuming the held list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 08:42:55 -05:00
co-authored by Cursor
parent 7d783bd97e
commit 8598dab516
5 changed files with 265 additions and 123 deletions
+145 -77
View File
@@ -52,11 +52,11 @@
</div>
<p v-if="error" class="rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-200">{{ error }}</p>
<p v-else-if="pauseArmed && anythingGenerating" class="rounded-2xl border border-amber-300/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">
Will pause after this video. The current clip keeps generating; the next queued job will not start until you resume.
<p v-else-if="willPauseLatch" class="rounded-2xl border border-amber-300/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">
Will pause after this video. The current clip keeps generating; this batch will stop after it finishes.
</p>
<p v-else-if="pauseArmed" class="rounded-2xl border border-amber-300/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">
Paused after current. Waiting jobs will not start until you press Resume queue.
<p v-else-if="heldPaused" class="rounded-2xl border border-amber-300/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">
This batch is paused. It will not continue until you resume.
</p>
<p v-else-if="!jobs.length && !orphans.length" class="glass rounded-3xl px-5 py-10 text-center text-sm text-zinc-400">
Nothing queued. Generate from Studio — a 30-shot script takes one slot, and you can line up more jobs behind it.
@@ -86,30 +86,32 @@
{{ job.cutIn ? 'Next after current shot' : 'Run next' }}
</button>
<template v-if="job.shots">
<button
type="button"
class="rounded-full bg-amber-400 px-3 py-1.5 text-xs font-semibold text-zinc-950 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === job.shots.id || job.shots.status === 'running' || job.shots.status === 'complete' || job.shots.pendingCount === 0"
@click="process(job.shots, 1)"
>
{{ continueLabel(job.shots) }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === job.shots.id || job.shots.status === 'running' || job.shots.status === 'complete' || job.shots.pendingCount < 2"
@click="process(job.shots, Math.min(3, job.shots.pendingCount))"
>
Next {{ Math.min(3, job.shots.pendingCount) || 3 }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === job.shots.id || job.shots.status === 'running' || job.shots.status === 'complete' || job.shots.pendingCount === 0"
@click="process(job.shots, 'all')"
>
Run remaining
</button>
<template v-if="canProcessShots(job)">
<button
type="button"
class="rounded-full bg-amber-400 px-3 py-1.5 text-xs font-semibold text-zinc-950 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === job.shots.id || job.shots.status === 'complete' || job.shots.pendingCount === 0"
@click="process(job.shots, 1)"
>
{{ continueLabel(job.shots) }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === job.shots.id || job.shots.status === 'complete' || job.shots.pendingCount < 2"
@click="process(job.shots, Math.min(3, job.shots.pendingCount))"
>
Next {{ Math.min(3, job.shots.pendingCount) || 3 }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === job.shots.id || job.shots.status === 'complete' || job.shots.pendingCount === 0"
@click="process(job.shots, 'all')"
>
Run remaining
</button>
</template>
<button
v-if="canPauseJob(job)"
type="button"
@@ -117,10 +119,10 @@
:class="pauseToggleClass(jobIsArmed(job))"
:disabled="pauseBusy"
:aria-pressed="jobIsArmed(job)"
:title="jobIsArmed(job) ? 'Click to keep going after this video' : 'Finish the current video, then stop'"
:title="jobPauseTitle(job)"
@click="togglePause({ job })"
>
{{ jobIsArmed(job) ? 'Will pause after this' : 'Pause after this' }}
{{ jobPauseLabel(job) }}
</button>
</template>
<button
@@ -130,10 +132,10 @@
:class="pauseToggleClass(jobIsArmed(job))"
:disabled="pauseBusy"
:aria-pressed="jobIsArmed(job)"
:title="jobIsArmed(job) ? 'Click to keep going after this video' : 'Finish the current video, then stop'"
:title="jobPauseTitle(job)"
@click="togglePause({ job })"
>
{{ jobIsArmed(job) ? 'Will pause after this' : 'Pause after this' }}
{{ jobPauseLabel(job) }}
</button>
<button
v-if="job.status === 'waiting' || job.status === 'held'"
@@ -225,41 +227,43 @@
<p v-if="queue.lastError" class="mt-1 text-xs text-red-300">{{ queue.lastError }}</p>
</div>
<div class="flex flex-wrap gap-2">
<button
type="button"
class="rounded-full bg-amber-400 px-3 py-1.5 text-xs font-semibold text-zinc-950 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === queue.id || queue.status === 'running' || queue.status === 'complete' || queue.pendingCount === 0"
@click="process(queue, 1)"
>
{{ continueLabel(queue) }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === queue.id || queue.status === 'running' || queue.status === 'complete' || queue.pendingCount < 2"
@click="process(queue, Math.min(3, queue.pendingCount))"
>
Next {{ Math.min(3, queue.pendingCount) || 3 }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === queue.id || queue.status === 'running' || queue.status === 'complete' || queue.pendingCount === 0"
@click="process(queue, 'all')"
>
Run remaining
</button>
<template v-if="canProcessQueue(queue)">
<button
type="button"
class="rounded-full bg-amber-400 px-3 py-1.5 text-xs font-semibold text-zinc-950 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === queue.id || queue.status === 'complete' || queue.pendingCount === 0"
@click="process(queue, 1)"
>
{{ continueLabel(queue) }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === queue.id || queue.status === 'complete' || queue.pendingCount < 2"
@click="process(queue, Math.min(3, queue.pendingCount))"
>
Next {{ Math.min(3, queue.pendingCount) || 3 }}
</button>
<button
type="button"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="busyId === queue.id || queue.status === 'complete' || queue.pendingCount === 0"
@click="process(queue, 'all')"
>
Run remaining
</button>
</template>
<button
v-if="queue.status === 'running' || queue.stopAfterCurrent"
type="button"
class="min-h-10 rounded-full px-3 py-2 text-xs font-semibold disabled:cursor-not-allowed disabled:opacity-50"
:class="pauseToggleClass(Boolean(queue.stopAfterCurrent) || queuePaused)"
:disabled="pauseBusy || (queue.status !== 'running' && !queue.stopAfterCurrent && !queuePaused)"
:aria-pressed="Boolean(queue.stopAfterCurrent) || queuePaused"
:title="(queue.stopAfterCurrent || queuePaused) ? 'Click to keep going after this video' : 'Finish the current video, then stop'"
:class="pauseToggleClass(queueIsArmed(queue))"
:disabled="pauseBusy"
:aria-pressed="queueIsArmed(queue)"
:title="queuePauseTitle(queue)"
@click="togglePause({ queue })"
>
{{ queue.stopAfterCurrent || queuePaused ? (queue.status === 'running' ? 'Will pause after this' : 'Paused after current') : 'Pause after this' }}
{{ queuePauseLabel(queue) }}
</button>
<button
v-if="queue.status !== 'running'"
@@ -387,22 +391,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
const canCutIn = computed(() => jobs.value.some(job => job.status === 'running' || job.status === 'held'))
const pauseBusy = computed(() => busyId.value === 'pause')
const anythingGenerating = computed(() => (
jobs.value.some(job => job.status === 'running' || job.shots?.status === 'running')
jobs.value.some(job => jobIsGenerating(job))
|| orphans.value.some(queue => queue.status === 'running')
))
const pauseArmed = computed(() => (
queuePaused.value
|| jobs.value.some(job => job.pauseAfterCurrent || job.pausedByUser || job.shots?.stopAfterCurrent)
|| orphans.value.some(queue => queue.stopAfterCurrent)
const willPauseLatch = computed(() => (
jobs.value.some(job => jobIsLatched(job))
|| orphans.value.some(queue => queueIsLatched(queue))
))
const heldPaused = computed(() => (
jobs.value.some(job => jobIsHeldPaused(job))
|| orphans.value.some(queue => queueIsHeldPaused(queue))
))
const pauseArmed = computed(() => willPauseLatch.value || heldPaused.value)
const pauseToggleLabel = computed(() => {
if (pauseArmed.value && anythingGenerating.value) return 'Will pause after this'
if (pauseArmed.value) return 'Resume queue'
if (willPauseLatch.value) return 'Will pause after this'
if (heldPaused.value) return 'Resume queue'
return 'Pause after current'
})
const pauseToggleTitle = computed(() => {
if (pauseArmed.value && anythingGenerating.value) return 'Click to keep going after this video'
if (pauseArmed.value) return 'Start the next waiting job'
if (willPauseLatch.value) return 'Click to keep going after this video'
if (heldPaused.value) return 'Resume the paused batch'
return 'Finish the current video, then stop. Does not cancel the one that’s generating.'
})
const pauseDisabledReason = computed(() => {
@@ -435,6 +443,9 @@ function progressLine(queue: Queue) {
const pauseNote = queue.stopAfterCurrent ? ' Will pause after this shot.' : ''
return `Generating shot ${next.index + 1} of ${queue.totalCount}. ${queue.completedCount} saved.${pauseNote}`
}
if (queueIsHeldPaused(queue) && next) {
return `Paused. ${queue.completedCount} of ${queue.totalCount} saved. Next is shot ${next.index + 1}.`
}
if (queue.completedCount === 0) return `Not started · ${queue.totalCount} shots`
if (next) return `Left off after shot ${queue.completedCount} of ${queue.totalCount}. Next is shot ${next.index + 1}.`
return `Shot ${queue.completedCount} of ${queue.totalCount}`
@@ -448,18 +459,17 @@ function jobLine(job: StudioJobRow) {
}
if (job.shots) {
const line = progressLine(job.shots)
if (job.pauseAfterCurrent && job.shots.status === 'running' && !job.shots.stopAfterCurrent) {
if (jobIsLatched(job) && !job.shots.stopAfterCurrent) {
return `${line} Will pause after this shot.`
}
if (job.pausedByUser) return `${line} Paused after current.`
return line
}
if (job.status === 'held') {
if (job.pausedByUser) return 'Paused after current. Next jobs will not start until you resume.'
if (jobIsHeldPaused(job)) return 'Paused. This batch will not continue until you resume.'
return job.holdForCutIn ? 'Paused so another job can cut in' : 'Paused'
}
if (job.status === 'running') {
if (job.pauseAfterCurrent) return `Generating · will pause after this · ${job.shotCount} shot${job.shotCount === 1 ? '' : 's'}`
if (jobIsLatched(job)) return `Generating · will pause after this · ${job.shotCount} shot${job.shotCount === 1 ? '' : 's'}`
return `Generating · ${job.shotCount} shot${job.shotCount === 1 ? '' : 's'}`
}
return job.status
@@ -630,24 +640,82 @@ function pauseToggleClass(pressed: boolean) {
: 'border border-amber-300/50 text-amber-100 hover:border-amber-200 hover:bg-amber-400/10'
}
function canPauseJob(job: StudioJobRow) {
function jobIsGenerating(job: StudioJobRow) {
return job.status === 'running' || job.shots?.status === 'running'
}
function jobIsLatched(job: StudioJobRow) {
return jobIsGenerating(job) && Boolean(job.pauseAfterCurrent || job.shots?.stopAfterCurrent)
}
function jobIsHeldPaused(job: StudioJobRow) {
if (jobIsGenerating(job)) return false
return Boolean(job.pausedByUser || job.shots?.stopAfterCurrent)
}
function queueIsLatched(queue: Queue) {
return queue.status === 'running' && Boolean(queue.stopAfterCurrent)
}
function queueIsHeldPaused(queue: Queue) {
return queue.status !== 'running' && Boolean(queue.stopAfterCurrent)
}
function queueIsArmed(queue: Queue) {
return Boolean(queue.stopAfterCurrent)
}
function canProcessShots(job: StudioJobRow) {
return Boolean(job.shots) && !jobIsGenerating(job) && !jobIsHeldPaused(job)
}
function canProcessQueue(queue: Queue) {
return queue.status !== 'running' && !queueIsHeldPaused(queue)
}
function canPauseJob(job: StudioJobRow) {
return jobIsGenerating(job) || jobIsHeldPaused(job)
}
function jobIsArmed(job: StudioJobRow) {
return Boolean(job.pauseAfterCurrent || job.shots?.stopAfterCurrent || (queuePaused.value && job.status === 'running'))
return jobIsLatched(job) || jobIsHeldPaused(job)
}
function jobPauseLabel(job: StudioJobRow) {
if (jobIsLatched(job)) return 'Will pause after this'
if (jobIsHeldPaused(job)) return 'Paused'
return 'Pause after this'
}
function jobPauseTitle(job: StudioJobRow) {
if (jobIsLatched(job)) return 'Click to keep going after this video'
if (jobIsHeldPaused(job)) return 'Resume this batch'
return 'Finish the current video, then stop'
}
function queuePauseLabel(queue: Queue) {
if (queueIsLatched(queue)) return 'Will pause after this'
if (queueIsHeldPaused(queue)) return 'Paused'
return 'Pause after this'
}
function queuePauseTitle(queue: Queue) {
if (queueIsLatched(queue)) return 'Click to keep going after this video'
if (queueIsHeldPaused(queue)) return 'Resume this batch'
return 'Finish the current video, then stop'
}
async function togglePause(target?: { job?: StudioJobRow; queue?: Queue }) {
const armed = target?.job
? jobIsArmed(target.job)
: target?.queue
? Boolean(target.queue.stopAfterCurrent || queuePaused.value)
? queueIsArmed(target.queue)
: pauseArmed.value
const resume = armed
if (target?.job) {
target.job.pauseAfterCurrent = !resume
if (target.job.shots) target.job.shots.stopAfterCurrent = !resume
if (resume) target.job.pausedByUser = false
}
if (target?.queue) target.queue.stopAfterCurrent = !resume
if (!target?.job && !target?.queue) queuePaused.value = !resume