Pause the queue after the current clip for real, and carry global locks plus labeled permanence stills through generate and shot prompts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 07:48:09 -05:00
co-authored by Cursor
parent 010ef6add2
commit efef409537
21 changed files with 1196 additions and 115 deletions
+53
View File
@@ -0,0 +1,53 @@
<template>
<details class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
<summary class="cursor-pointer text-sm font-medium text-zinc-200">
Global locks
<span class="ml-2 font-normal text-xs text-zinc-500">Every shot inherits these. Each clip adds one beat.</span>
</summary>
<p class="mt-2 text-xs text-zinc-500">
Shared standards for last-frame I2V. Later shots get a compact <span class="text-zinc-300">[INHERITS]</span> line, not this whole block again.
</p>
<textarea
:value="modelValue"
rows="10"
class="mt-3 max-h-64 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2"
:placeholder="placeholder"
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
/>
<div class="mt-2 flex flex-wrap gap-2">
<button
type="button"
class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100"
@click="insertTemplate"
>
Insert template
</button>
<button
v-if="modelValue.trim()"
type="button"
class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-400 hover:text-white"
@click="emit('update:modelValue', '')"
>
Clear
</button>
</div>
</details>
</template>
<script setup lang="ts">
import { DEFAULT_GLOBAL_LOCKS } from '~/utils/globalLocks'
defineProps<{
modelValue: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const placeholder = DEFAULT_GLOBAL_LOCKS
function insertTemplate() {
emit('update:modelValue', DEFAULT_GLOBAL_LOCKS)
}
</script>
+10 -4
View File
@@ -172,13 +172,15 @@ const props = defineProps<{
folderId: string
hideInputPreview?: boolean
heading?: string
stillOnly?: boolean
}>()
const emit = defineEmits<{
close: []
updated: [payload: unknown]
error: [message: string]
select: [file: File, hidden: boolean]
select: [file: File, hidden: boolean, stillId?: string]
pick: [still: { id: string; filename: string; hideInput?: boolean }]
}>()
const fileInput = ref<HTMLInputElement | null>(null)
@@ -262,8 +264,10 @@ async function ingest(file: File) {
body.append('folderId', current.value?.id || activeFolderId.value)
body.append('hideInput', String(props.hideInputPreview === true))
try {
emit('updated', await $fetch('/api/library/stills', { method: 'POST', body }))
emit('select', file, props.hideInputPreview === true)
const result = await $fetch<{ still?: { id: string; filename: string; hideInput?: boolean } }>('/api/library/stills', { method: 'POST', body })
emit('updated', result)
if (result.still?.id) emit('pick', result.still)
emit('select', file, props.hideInputPreview === true, result.still?.id)
} catch (error: any) {
emit('error', error?.data?.statusMessage || 'Could not save image')
}
@@ -283,10 +287,12 @@ function onDrop(event: DragEvent) {
}
async function pickStill(still: PickerStill) {
emit('pick', { id: still.id, filename: still.filename, hideInput: still.hideInput })
if (props.stillOnly) return
try {
const blob = await $fetch<Blob>(`/api/library/stills/${still.id}`, { responseType: 'blob' })
const file = new File([blob], still.filename, { type: blob.type || 'image/png' })
emit('select', file, still.hideInput === true)
emit('select', file, still.hideInput === true, still.id)
} catch (error: any) {
emit('error', error?.data?.statusMessage || 'Could not load that still')
}
+81
View File
@@ -0,0 +1,81 @@
<template>
<div>
<div v-if="label" class="mb-1.5 flex items-baseline justify-between gap-2">
<p class="text-[11px] font-medium uppercase tracking-wide text-zinc-500">{{ label }}</p>
<p v-if="hint" class="text-[11px] text-zinc-600">{{ hint }}</p>
</div>
<div class="flex flex-wrap items-start gap-1.5">
<div
v-for="(item, index) in refs"
:key="item.stillId + '-' + index"
class="flex w-14 flex-col items-center gap-0.5"
>
<span class="relative h-12 w-12 overflow-hidden rounded-lg bg-zinc-900">
<span v-if="hidden" class="sensitive-glow block h-full w-full" />
<img
v-else
:src="`/api/library/stills/${item.stillId}?thumb=1`"
alt=""
loading="lazy"
decoding="async"
class="h-full w-full object-cover"
>
<button
v-if="!disabled"
type="button"
class="absolute right-0 top-0 rounded-bl-md bg-zinc-950/80 px-1 text-[10px] leading-4 text-zinc-300 hover:text-red-200"
title="Remove"
@click.stop="emit('remove', index)"
>
×
</button>
</span>
<select
class="w-full truncate rounded border border-white/10 bg-zinc-950 px-0.5 py-0 text-[10px] text-zinc-400"
:disabled="disabled"
:value="item.label"
@change="emit('updateLabel', index, ($event.target as HTMLSelectElement).value)"
>
<option v-for="option in labelOptions(item.label)" :key="option" :value="option">{{ option }}</option>
</select>
</div>
<button
v-if="!disabled && refs.length < max"
type="button"
class="flex h-12 w-12 flex-col items-center justify-center rounded-lg border border-dashed border-white/15 text-[10px] text-zinc-500 hover:border-amber-300/40 hover:text-amber-100"
:title="addTitle"
@click="emit('add')"
>
+ Ref
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { PERMANENCE_REF_LABELS, type PermanenceRef } from '~/utils/globalLocks'
const props = withDefaults(defineProps<{
refs: PermanenceRef[]
label?: string
hint?: string
hidden?: boolean
disabled?: boolean
max?: number
addTitle?: string
}>(), {
max: 6,
addTitle: 'Attach a permanence still'
})
defineEmits<{
add: []
remove: [index: number]
updateLabel: [index: number, label: string]
}>()
function labelOptions(current: string) {
if (PERMANENCE_REF_LABELS.includes(current as typeof PERMANENCE_REF_LABELS[number])) return [...PERMANENCE_REF_LABELS]
return [...PERMANENCE_REF_LABELS, current]
}
</script>