Let queued video, image, and music jobs open in an inspect modal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-03 00:15:02 -05:00
co-authored by Cursor
parent 53990192e4
commit f05f8f3597
12 changed files with 1257 additions and 37 deletions
+21 -25
View File
@@ -11,7 +11,7 @@
<div>
<h2 class="font-display text-2xl font-bold">Queue</h2>
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
Jobs you submitted from Studio or Music. A long shot script is one job; its shots are the list underneath. Run next inserts a waiting job after the current shot as its own clip.
Click a job to inspect it, fix a waiting one, copy it, or save a template. A long shot script is one job; its shots are the list underneath.
</p>
</div>
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
@@ -57,7 +57,7 @@
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex items-start gap-3">
<button type="button" class="flex min-w-0 items-start gap-3 text-left" @click="inspectJobId = job.id">
<img
v-if="jobThumb(job)"
:src="jobThumb(job)"
@@ -65,18 +65,18 @@
class="h-12 w-12 shrink-0 rounded-lg bg-black/40 object-cover"
>
<div class="min-w-0">
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ jobKindLabel(job) }}</p>
<h3 class="truncate font-display text-lg font-semibold text-zinc-100">{{ job.name }}</h3>
<p class="mt-1 text-sm text-zinc-300">{{ jobLine(job) }}</p>
<p v-if="job.shots && liveMessage[job.shots.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.shots.id] }}</p>
<p v-else-if="liveMessage[job.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.id] }}</p>
<p v-if="job.lastError" class="mt-1 text-xs text-red-300">{{ job.lastError }}</p>
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ jobKindLabel(job) }}</p>
<h3 class="truncate font-display text-lg font-semibold text-zinc-100 hover:text-white">{{ job.name }}</h3>
<p class="mt-1 text-sm text-zinc-300">{{ jobLine(job) }}</p>
<p v-if="job.shots && liveMessage[job.shots.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.shots.id] }}</p>
<p v-else-if="liveMessage[job.id]" class="mt-1 text-xs text-amber-100">{{ liveMessage[job.id] }}</p>
<p v-if="job.lastError" class="mt-1 text-xs text-red-300">{{ job.lastError }}</p>
</div>
</div>
</button>
</div>
<div class="flex flex-wrap gap-2">
<button
v-if="job.status === 'waiting'"
v-if="job.status === 'waiting' && job.kind !== 'music'"
type="button"
class="rounded-full border border-sky-300/40 px-3 py-1.5 text-xs text-sky-100 hover:border-sky-200 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="Boolean(busyId)"
@@ -410,6 +410,11 @@
</div>
</div>
</Teleport>
<QueuedJobModal
:job-id="inspectJobId"
@close="inspectJobId = ''"
@saved="loadQueues"
/>
</div>
</template>
@@ -477,6 +482,7 @@ type PendingRemove = {
const videoLoras = ref<string[]>([])
const loraTriggers = ref<Record<string, string>>({})
const loraAliases = ref<Record<string, string>>({})
const inspectJobId = ref('')
const jobs = ref<StudioJobRow[]>([])
const orphans = ref<Queue[]>([])
const waitingCount = ref(0)
@@ -573,20 +579,10 @@ function progressLine(queue: Queue) {
function jobLine(job: StudioJobRow) {
if (job.kind === 'music') {
if (job.status === 'waiting') {
return job.cutIn
? 'Next after the current shot · ACE-Step'
: 'Waiting · ACE-Step'
}
if (job.status === 'held') {
if (jobIsHeldPaused(job)) return 'Paused. This track will not continue until you resume.'
return job.holdForCutIn ? 'Paused so another job can cut in' : 'Paused'
}
if (job.status === 'running') {
if (jobIsLatched(job)) return 'Generating track · will pause after this'
return 'Generating track'
}
return job.status
if (job.status === 'waiting') return job.cutIn ? 'Next after the current shot · ACE-Step' : 'Waiting · ACE-Step'
if (job.status === 'held') return 'Paused'
if (job.status === 'error') return job.lastError || 'Failed'
return 'Generating track'
}
if (job.kind === 'edit') {
const passes = job.shotCount || 1
@@ -822,7 +818,7 @@ function jobKindLabel(job: StudioJobRow) {
if (job.imagePipeline === 'v2') return 'IMAGE V2'
return (job.shotCount || 1) > 1 ? 'EDIT' : 'IMAGE'
}
return 'Job'
return 'VIDEO'
}
function jobThumb(job: StudioJobRow) {