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:
+145
-77
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user