Add same-mode extra passes to Image v2, and hide Shot script from image Recommend.

Library tiles show a check when selected, and xAIGen can hide a preview from the details pane.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-30 18:17:17 -05:00
co-authored by Cursor
parent fd9831dfcf
commit e89b47b5f9
8 changed files with 500 additions and 206 deletions
+124 -29
View File
@@ -139,32 +139,40 @@
<div v-if="studioMode === 'editv2'" class="flex flex-wrap gap-2">
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium"
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
:class="v2Mode === 'edit' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
:disabled="v2ModeLocked && v2Mode !== 'edit'"
:title="v2ModeLocked && v2Mode !== 'edit' ? 'Remove extra passes to change graphs' : ''"
@click="setV2Mode('edit')"
>
Edit
</button>
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium"
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
:class="v2Mode === 'compose' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
:disabled="v2ModeLocked && v2Mode !== 'compose'"
:title="v2ModeLocked && v2Mode !== 'compose' ? 'Remove extra passes to change graphs' : ''"
@click="setV2Mode('compose')"
>
Compose
</button>
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium"
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
:class="v2Mode === 'refine' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
:disabled="v2ModeLocked && v2Mode !== 'refine'"
:title="v2ModeLocked && v2Mode !== 'refine' ? 'Remove extra passes to change graphs' : ''"
@click="setV2Mode('refine')"
>
Refine
</button>
<button
type="button"
class="rounded-full border px-3 py-1 text-xs font-medium"
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
:class="v2Mode === 'generate' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
:disabled="v2ModeLocked && v2Mode !== 'generate'"
:title="v2ModeLocked && v2Mode !== 'generate' ? 'Remove extra passes to change graphs' : ''"
@click="setV2Mode('generate')"
>
Generate
@@ -244,7 +252,7 @@
:disabled="recommendBusy"
:title="recommendHint"
>
<option v-for="item in promptPresets" :key="item.id" :value="item.id">{{ item.name }}</option>
<option v-for="item in visiblePromptPresets" :key="item.id" :value="item.id">{{ item.name }}</option>
</select>
<button
type="button"
@@ -376,7 +384,7 @@
</button>
</div>
</div>
<div v-if="studioMode === 'edit'" class="mt-3 space-y-3">
<div v-if="studioMode === 'edit' || v2PassChaining" class="mt-3 space-y-3">
<div
v-for="(item, index) in editPassQueue"
:key="item.id"
@@ -397,7 +405,7 @@
v-model="item.prompt"
rows="3"
class="w-full 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 focus:ring-2"
placeholder="Next edit prompt, applied to the previous pass output…"
:placeholder="extraPassPlaceholder"
/>
</div>
<button
@@ -406,8 +414,9 @@
:disabled="editBusy"
@click="queueEditPass"
>
+ Pass
{{ extraPassButtonLabel }}
</button>
<p class="text-xs text-zinc-500">{{ extraPassHint }}</p>
</div>
<details v-if="identityPrompting && prompt.trim()" class="mt-2 rounded-xl border border-white/10 bg-zinc-950/40 px-3 py-2">
<summary class="cursor-pointer text-xs text-zinc-400">Prompt sent to Reference to Video</summary>
@@ -1561,14 +1570,27 @@
>
<template v-if="entry.kind === 'clip'">
<div class="flex gap-3">
<label class="flex shrink-0 items-start pt-1" @click.stop>
<input
type="checkbox"
class="mt-0.5 h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
:checked="selectedKeys.includes(entry.key)"
@click.prevent="onSelectClick(entry.key, $event)"
<button
type="button"
role="checkbox"
class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center self-start rounded-[4px] border"
:class="selectedKeys.includes(entry.key)
? 'border-white bg-amber-400 text-zinc-950'
: 'border-white/30 bg-black/45'"
:aria-checked="selectedKeys.includes(entry.key)"
:aria-label="selectedKeys.includes(entry.key) ? 'Deselect' : 'Select'"
@click.stop="onSelectClick(entry.key, $event)"
>
<svg
v-if="selectedKeys.includes(entry.key)"
class="h-3 w-3"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
</label>
<path d="M3.2 8.2 6.4 11.3 12.8 4.6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button
type="button"
class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900"
@@ -1742,14 +1764,27 @@
</template>
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
<div class="flex gap-3">
<label class="flex shrink-0 items-start pt-1" @click.stop>
<input
type="checkbox"
class="mt-0.5 h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
:checked="selectedKeys.includes(entry.key)"
@click.prevent="onSelectClick(entry.key, $event)"
<button
type="button"
role="checkbox"
class="mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center self-start rounded-[4px] border"
:class="selectedKeys.includes(entry.key)
? 'border-white bg-amber-400 text-zinc-950'
: 'border-white/30 bg-black/45'"
:aria-checked="selectedKeys.includes(entry.key)"
:aria-label="selectedKeys.includes(entry.key) ? 'Deselect' : 'Select'"
@click.stop="onSelectClick(entry.key, $event)"
>
<svg
v-if="selectedKeys.includes(entry.key)"
class="h-3 w-3"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
>
</label>
<path d="M3.2 8.2 6.4 11.3 12.8 4.6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openStill(entry.stillGroup.head)">
<span v-if="entry.stillGroup.head.hideInput" class="sensitive-glow block h-full w-full" />
<img
@@ -2271,6 +2306,10 @@ const promptPresets = ref<Array<{ id: string; name: string; description?: string
{ id: 'enhance', name: 'Enhance', builtin: true },
{ id: 'shot-script', name: 'Shot script', builtin: true }
])
const visiblePromptPresets = computed(() => {
if (studioMode.value === 'video') return promptPresets.value
return promptPresets.value.filter(item => item.id !== 'shot-script')
})
const recommendBusy = ref(false)
const recommendText = ref('')
const recommendError = ref('')
@@ -2849,6 +2888,7 @@ const editV2Blocked = computed(() => {
if (v2Mode.value !== 'generate' && !file.value) return true
if (v2Mode.value === 'compose' && !editRefFile.value) return true
if (v2Mode.value === 'refine' && !refineMaskDirty.value) return true
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return true
return false
})
const editV2BlockReason = computed(() => {
@@ -2860,14 +2900,37 @@ const editV2BlockReason = computed(() => {
? 'Write a prompt first.'
: 'Write a prompt, or pick Landscape / Portrait / Square to fill the frame.'
}
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) return 'Each extra pass needs a prompt.'
if (!folderId.value) return 'Choose a library folder before generating.'
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image v2 is not configured. Set COMFY_HOST.'
return ''
})
const v2PassChaining = computed(() => studioMode.value === 'editv2' && (v2Mode.value === 'edit' || v2Mode.value === 'compose'))
const v2ModeLocked = computed(() => v2PassChaining.value && editPassQueue.value.length > 0)
const extraPassButtonLabel = computed(() => (
studioMode.value === 'editv2' && v2Mode.value === 'compose' ? '+ Compose pass' : '+ Edit pass'
))
const extraPassHint = computed(() => {
if (studioMode.value === 'editv2' && v2Mode.value === 'compose') {
return 'Each extra pass is another Compose on the last output, with the same still B. You cannot switch graphs between passes.'
}
return 'Each extra pass is another Edit on the last output. You cannot switch graphs between passes.'
})
const extraPassPlaceholder = computed(() => (
studioMode.value === 'editv2' && v2Mode.value === 'compose'
? 'Next compose prompt, applied to the previous pass output…'
: 'Next edit prompt, applied to the previous pass output…'
))
const editV2SubmitLabel = computed(() => {
const occupied = videoBusy.value || editBusy.value || studioJobs.value.some(job => job.status === 'running')
const n = v2PassChaining.value ? editPassQueue.value.length : 0
const label = v2Mode.value === 'generate' ? 'Generate image' : v2Mode.value === 'refine' ? 'Refine image' : v2Mode.value === 'compose' ? 'Compose image' : 'Edit image'
return occupied ? `Queue ${label.toLowerCase()}` : label
if (!occupied) {
if (!n) return label
return n === 1 ? `${label} + 1 pass` : `${label} + ${n} passes`
}
if (n) return `Queue ${n + 1} passes`
return `Queue ${label.toLowerCase()}`
})
const generateLabel = computed(() => {
const n = queuedExtensionCount.value
@@ -3582,6 +3645,12 @@ watch(recommendPresetId, (id) => {
try { localStorage.setItem('aigen-prompt-preset', id) } catch { /* ignore */ }
})
watch(studioMode, () => {
if (studioMode.value !== 'video' && recommendPresetId.value === 'shot-script') {
recommendPresetId.value = 'enhance'
}
})
watch([browseFolderId, libraryFilter], () => {
selectedKeys.value = []
lastSelectedKey.value = ''
@@ -3987,6 +4056,7 @@ function applyGenerationPreset(preset: GenerationPreset) {
const settings = preset.settings as ImageV2PresetSettings
persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
v2Mode.value = settings.mode === 'compose' ? 'compose' : settings.mode === 'refine' ? 'refine' : settings.mode === 'generate' ? 'generate' : 'edit'
if (v2Mode.value !== 'edit' && v2Mode.value !== 'compose') editPassQueue.value = []
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
if (typeof settings.prompt === 'string') prompt.value = settings.prompt
@@ -4203,6 +4273,9 @@ async function loadPromptPresets() {
} else if (!promptPresets.value.some(item => item.id === recommendPresetId.value)) {
recommendPresetId.value = promptPresets.value[0]?.id || 'enhance'
}
if (studioMode.value !== 'video' && recommendPresetId.value === 'shot-script') {
recommendPresetId.value = 'enhance'
}
}
async function recommendPrompt() {
@@ -4220,7 +4293,7 @@ async function recommendPrompt() {
body.append('presetId', recommendPresetId.value)
body.append(
'mode',
studioMode.value === 'edit' ? 'edit' : (shotScriptMode.value ? 'shot-script' : 'video')
studioMode.value === 'edit' || studioMode.value === 'editv2' ? 'edit' : (shotScriptMode.value ? 'shot-script' : 'video')
)
try {
const started = await $fetch<PromptRecommendSnap>('/api/prompt/recommend', { method: 'POST', body })
@@ -4258,7 +4331,7 @@ function applyRecommend() {
promptBeforeRecommend.value = prompt.value
editPassesBeforeRecommend.value = editPassQueue.value.map(item => ({ ...item }))
shotScriptBeforeRecommend.value = shotScriptMode.value
if (studioMode.value === 'edit') {
if (studioMode.value === 'edit' || v2PassChaining.value) {
const passes = parseEditPasses(text)
if (passes && passes.length > 1) {
prompt.value = passes[0]
@@ -5909,6 +5982,11 @@ async function editImage() {
}
function setV2Mode(mode: ImageV2Mode) {
if (v2Mode.value === mode) return
if (editPassQueue.value.length && v2PassChaining.value) {
toast('Remove extra passes to change graphs. Extra passes stay in the current mode.')
return
}
v2Mode.value = mode
if (mode === 'edit') v2Task.value = 'scene'
if (mode === 'refine') {
@@ -5994,6 +6072,7 @@ async function ensureRefineCanvas() {
async function sendToEdit() {
if (!editResultUrl.value) return
editPassQueue.value = []
studioMode.value = 'editv2'
outputFocus.value = 'edit'
try {
@@ -6007,6 +6086,7 @@ async function sendToEdit() {
async function sendToRefine() {
if (!editResultUrl.value) return
editPassQueue.value = []
studioMode.value = 'editv2'
outputFocus.value = 'edit'
try {
@@ -6031,6 +6111,11 @@ async function editImageV2() {
toast('Paint a mask on Still A first. Refine will not fall back to Edit.')
return
}
if (v2PassChaining.value && editPassQueue.value.some(item => !item.prompt.trim())) {
toast('Each extra pass needs a prompt.')
return
}
const extraPasses = v2PassChaining.value ? editPassQueue.value.map(item => ({ prompt: item.prompt.trim() })) : []
const hideOut = hideThumbnail.value
try {
const body = new FormData()
@@ -6072,6 +6157,7 @@ async function editImageV2() {
body.append('hideThumbnail', String(showPrivacyToggles.value && hideThumbnail.value))
body.append('name', clipName.value.trim())
if (inputStillId.value) body.append('stillId', inputStillId.value)
if (extraPasses.length) body.append('passes', JSON.stringify(extraPasses))
const v2Stack = filterLoraStackForImageEngine(
filterLoraStackForStudio(imageV2LoraStack.value, ltxEnabled.value),
v2Engine.value
@@ -6097,7 +6183,10 @@ async function editImageV2() {
body
})
if (started.queued) {
toast(v2Engine.value === 'krea' ? 'Queued Krea generate behind the current job.' : 'Queued Image v2 behind the current job.')
const n = 1 + extraPasses.length
toast(n > 1
? `Queued ${n} Image v2 passes behind the current job.`
: (v2Engine.value === 'krea' ? 'Queued Krea generate behind the current job.' : 'Queued Image v2 behind the current job.'))
await refreshStudioQueue()
return
}
@@ -6113,11 +6202,17 @@ async function editImageV2() {
editStatusMessage.value = 'Checking Beast ComfyUI...'
editMaxStep.value = started.steps
editChainStep.value = 1
editChainTotal.value = 1
editChainLabel.value = ''
editChainTotal.value = 1 + extraPasses.length
editChainLabel.value = extraPasses.length ? 'Pass 1' : ''
editOverallProgress.value = 0
editCompletedChainStep.value = 0
editActiveChainPlan.value = [{ label: v2Mode.value === 'generate' ? 'Generate' : v2Mode.value === 'refine' ? 'Refine' : v2Mode.value === 'compose' ? 'Compose' : 'Edit', prompt: prompt.value.trim() }]
const passNoun = v2Mode.value === 'compose' ? 'Compose' : v2Mode.value === 'refine' ? 'Refine' : v2Mode.value === 'generate' ? 'Generate' : 'Edit'
editActiveChainPlan.value = extraPasses.length
? [
{ label: 'Pass 1', prompt: prompt.value.trim() },
...extraPasses.map((item, index) => ({ label: `Pass ${index + 2}`, prompt: item.prompt }))
]
: [{ label: passNoun, prompt: prompt.value.trim() }]
startTimer('edit')
editDownloadName.value = downloadName
editJobId.value = started.jobId