Move library collections as a group and map v2 identity refs.

Collapsed collection actions now move, rename, delete, and rerun every part so chains stay together. Identity mode follows the v2u3 Ref2VA picture wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-26 22:56:37 -05:00
co-authored by Cursor
parent f54e8da857
commit e334cb7a7a
28 changed files with 1125 additions and 293 deletions
+166 -14
View File
@@ -1,17 +1,22 @@
import { AsyncLocalStorage } from 'node:async_hooks'
import { getComfyHost } from '~/server/utils/comfy'
const imageHostAls = new AsyncLocalStorage<string>()
let imageComfyHostOverride = ''
export type ImageEditBox = 'beast' | 'sidecar'
export function withImageComfyHost<T>(host: string, fn: () => Promise<T>) {
return imageHostAls.run(String(host || '').replace(/\/$/, ''), fn)
}
export function setImageComfyHostOverride(url: string) {
imageComfyHostOverride = String(url || '').replace(/\/$/, '')
}
export function getImageComfyHost() {
if (imageComfyHostOverride) return imageComfyHostOverride
const config = useRuntimeConfig()
let host = String(config.imageComfyHost || process.env.IMAGE_COMFY_HOST || '').trim().replace(/\/$/, '')
const port = String(config.imageComfyPort || process.env.IMAGE_COMFY_PORT || '').trim()
if (!host) {
throw createError({ statusCode: 500, statusMessage: 'IMAGE_COMFY_HOST is not configured' })
}
function normalizeHost(raw: string, port = '') {
let host = String(raw || '').trim().replace(/\/$/, '')
if (!host) return ''
if (!/^https?:\/\//i.test(host)) host = `http://${host}`
try {
const url = new URL(host)
@@ -22,13 +27,46 @@ export function getImageComfyHost() {
}
}
export function imageComfyConfigured() {
export function getBeastImageHost() {
const config = useRuntimeConfig()
return Boolean(String(config.imageComfyHost || process.env.IMAGE_COMFY_HOST || '').trim())
const explicit = String(config.imageComfyPrimaryHost || process.env.IMAGE_COMFY_PRIMARY_HOST || '').trim()
if (explicit) return normalizeHost(explicit)
try {
return getComfyHost()
} catch {
return ''
}
}
export function getSidecarImageHost() {
const config = useRuntimeConfig()
const fallback = String(config.imageComfyFallbackHost || process.env.IMAGE_COMFY_FALLBACK_HOST || '').trim()
if (fallback) return normalizeHost(fallback)
const legacy = String(config.imageComfyHost || process.env.IMAGE_COMFY_HOST || '').trim()
const port = String(config.imageComfyPort || process.env.IMAGE_COMFY_PORT || '').trim()
return normalizeHost(legacy, port)
}
export function getImageComfyHost() {
const fromAls = imageHostAls.getStore()
if (fromAls) return fromAls
if (imageComfyHostOverride) return imageComfyHostOverride
return getBeastImageHost() || getSidecarImageHost()
}
export function imageComfyConfigured() {
return Boolean(getBeastImageHost() || getSidecarImageHost())
}
export function sameImageHost(a: string, b: string) {
return Boolean(a && b && a.replace(/\/$/, '') === b.replace(/\/$/, ''))
}
export async function imageComfyFetch(path: string, init?: RequestInit) {
const host = getImageComfyHost()
if (!host) {
throw createError({ statusCode: 500, statusMessage: 'No image ComfyUI host is configured' })
}
try {
return await fetch(`${host}${path}`, init)
} catch (error) {
@@ -88,10 +126,14 @@ export async function queueImagePrompt(graph: unknown, clientId: string) {
return payload as { prompt_id: string; number?: number }
}
export async function interruptImageComfy() {
if (!imageComfyConfigured()) return false
const res = await imageComfyFetch('/interrupt', { method: 'POST' })
return res.ok
export async function interruptImageComfy(host?: string) {
if (!imageComfyConfigured() && !host) return false
const run = async () => {
const res = await imageComfyFetch('/interrupt', { method: 'POST' })
return res.ok
}
if (host) return withImageComfyHost(host, run)
return run()
}
export async function fetchImageHistory(promptId: string) {
@@ -253,3 +295,113 @@ export async function waitForImageEdit(opts: {
})
}
function purgeImageEnabled() {
return useRuntimeConfig().purgeComfyOutputs !== false
}
async function deleteSidecarFile(file: { filename: string; subfolder?: string; type?: string }) {
const payload = {
filename: file.filename,
subfolder: file.subfolder || '',
type: file.type || 'output'
}
for (const path of ['/delete', '/aigen/purge']) {
try {
const res = await imageComfyFetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(4000)
})
if (res.ok) return true
} catch {
/* Comfy may not expose this route */
}
}
return false
}
async function purgeImageOnDesktop(opts: {
inputName?: string
inputSubfolder?: string
output?: { filename: string; subfolder: string; type: string }
}) {
const config = useRuntimeConfig()
const host = getImageComfyHost()
const sidecar = getSidecarImageHost()
const useSidecarAgent = sidecar && sameImageHost(host, sidecar)
const controlUrl = String(
useSidecarAgent
? (config.imageComfyControlUrl || process.env.IMAGE_COMFY_CONTROL_URL || '')
: (config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '')
).replace(/\/$/, '')
const token = String(
useSidecarAgent
? (config.imageComfyControlToken || process.env.IMAGE_COMFY_CONTROL_TOKEN || '')
: (config.comfyControlToken || process.env.COMFY_CONTROL_TOKEN || '')
)
if (!controlUrl) return
try {
await fetch(`${controlUrl}/purge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {})
},
body: JSON.stringify({
imageName: opts.inputName || '',
imageSubfolder: opts.inputSubfolder || '',
output: opts.output || null
}),
signal: AbortSignal.timeout(4000)
})
} catch {
/* Sidecar host agent is optional; Comfy HTTP delete is the primary path. */
}
}
export async function purgeImageComfyArtifacts(opts: {
output?: { filename: string; subfolder: string; type: string }
inputName?: string
inputSubfolder?: string
promptId?: string
}) {
if (!purgeImageEnabled()) return
const files: { filename: string; subfolder: string; type: string }[] = []
if (opts.output?.filename) {
files.push({
filename: opts.output.filename,
subfolder: opts.output.subfolder || '',
type: opts.output.type || 'output'
})
}
if (opts.inputName) {
files.push({
filename: opts.inputName,
subfolder: opts.inputSubfolder || '',
type: 'input'
})
}
for (const file of files) {
await deleteSidecarFile(file)
}
await purgeImageOnDesktop({
inputName: opts.inputName,
inputSubfolder: opts.inputSubfolder,
output: opts.output
})
if (opts.promptId) {
try {
await imageComfyFetch('/history', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ delete: [opts.promptId] }),
signal: AbortSignal.timeout(4000)
})
} catch {
/* ignore */
}
}
}