Start Sidecar Qwen VL from Recommend when the prompt enhancer is offline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-27 22:56:29 -05:00
co-authored by Cursor
parent b2c59c3423
commit 91948dd329
3 changed files with 126 additions and 4 deletions
+5 -3
View File
@@ -1848,14 +1848,13 @@ const recommendDisabled = computed(() => {
if (recommendBusy.value) return true
if (!file.value || !prompt.value.trim()) return true
if (!imageSidecarConfigured.value) return true
if (!imageSidecarOk.value) return true
return false
})
const recommendHint = computed(() => {
if (!imageSidecarConfigured.value) return 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST.'
if (!imageSidecarOk.value) return 'Sidecar Qwen VL is offline.'
if (!file.value) return 'Load a still first.'
if (!prompt.value.trim()) return 'Type a draft prompt to enhance.'
if (!imageSidecarOk.value) return 'Sidecar is offline. Recommend will start Qwen VL on 192.168.77.101.'
if (imageSidecarBusy.value) return 'Sidecar is busy — this may wait in its queue.'
return 'Send this still and draft to Sidecar Qwen VL.'
})
@@ -2507,7 +2506,9 @@ async function recommendPrompt() {
recommendError.value = ''
recommendText.value = ''
recommendApplied.value = false
recommendMessage.value = 'Uploading still to Sidecar Qwen VL…'
recommendMessage.value = imageSidecarOk.value
? 'Uploading still to Sidecar Qwen VL…'
: 'Sidecar is offline. Starting Qwen VL…'
const body = new FormData()
body.append('image', file.value)
body.append('prompt', prompt.value)
@@ -2542,6 +2543,7 @@ async function recommendPrompt() {
} finally {
recommendBusy.value = false
if (recommendJobId.value) recommendJobId.value = ''
void pollComfyHealth()
}
}
+116 -1
View File
@@ -1,4 +1,4 @@
import { getImageComfyHost, setImageComfyHostOverride, imageComfyConfigured, getBeastImageHost, withImageComfyHost, type ImageEditBox } from '~/server/utils/imageComfy'
import { getImageComfyHost, setImageComfyHostOverride, imageComfyConfigured, getBeastImageHost, getSidecarImageHost, withImageComfyHost, type ImageEditBox } from '~/server/utils/imageComfy'
export type ImageComfyState = 'online' | 'booting' | 'busy' | 'starting' | 'offline'
@@ -229,6 +229,121 @@ export function ensureImageComfyReady(onStatus: StatusFn = () => undefined) {
return run
}
let sidecarGate: Promise<unknown> = Promise.resolve()
let sidecarLastStartAt = 0
function sidecarControlSettings() {
const config = useRuntimeConfig()
return {
host: getSidecarImageHost(),
controlUrl: String(config.imageComfyControlUrl || process.env.IMAGE_COMFY_CONTROL_URL || '').replace(/\/$/, ''),
controlToken: String(config.imageComfyControlToken || process.env.IMAGE_COMFY_CONTROL_TOKEN || ''),
healthTimeoutMs: 2500,
startTimeoutMs: Number(process.env.IMAGE_COMFY_START_TIMEOUT_MS || 180_000)
}
}
async function sidecarControlRequest(path: string, method = 'GET') {
const { controlUrl, controlToken } = sidecarControlSettings()
if (!controlUrl) return null
try {
const res = await fetch(`${controlUrl}${path}`, {
method,
headers: {
Accept: 'application/json',
...(controlToken ? { Authorization: `Bearer ${controlToken}` } : {})
},
signal: AbortSignal.timeout(5000)
})
if (!res.ok) return { ok: false, status: res.status }
return await res.json() as {
ok?: boolean
http?: boolean
process?: boolean
started?: boolean
already?: boolean
}
} catch {
return null
}
}
async function startSidecarComfy() {
const now = Date.now()
if (now - sidecarLastStartAt < 15_000) {
log('sidecar-start-skipped', { reason: 'recent-start' })
return { started: false, reason: 'recent-start' as const }
}
sidecarLastStartAt = now
const remote = await sidecarControlRequest('/start', 'POST')
if (remote?.ok || remote?.started) {
log('sidecar-start-remote', {})
return { started: true, reason: 'remote' as const }
}
return { started: false, reason: 'no-launcher' as const }
}
async function ensureSidecarUnlocked(onStatus: StatusFn) {
const cfg = sidecarControlSettings()
const host = cfg.host
if (!host) {
throw createError({
statusCode: 503,
statusMessage: 'Sidecar Qwen VL is not configured. Set IMAGE_COMFY_HOST or IMAGE_COMFY_FALLBACK_HOST.'
})
}
onStatus({ state: 'offline', message: 'Checking Sidecar Qwen VL…', host, box: 'sidecar' })
const health = await checkImageComfyHttp(cfg.healthTimeoutMs, host)
if (health.ok) {
onStatus({ state: 'online', message: 'Sidecar Qwen VL online', host, box: 'sidecar' })
return
}
onStatus({
state: 'starting',
message: 'Sidecar is offline. Starting Qwen VL on 192.168.77.101…',
host,
box: 'sidecar'
})
const started = await startSidecarComfy()
if (!started.started && started.reason === 'no-launcher') {
throw createError({
statusCode: 503,
statusMessage: 'Sidecar is offline and the host agent did not start it. Confirm the agent on 192.168.77.101:8199 is running.'
})
}
const deadline = Date.now() + cfg.startTimeoutMs
let attempt = 0
while (Date.now() < deadline) {
attempt += 1
onStatus({
state: 'starting',
message: `Starting Sidecar Qwen VL… waiting for Comfy (${attempt})`,
host,
box: 'sidecar'
})
const next = await checkImageComfyHttp(cfg.healthTimeoutMs, host)
if (next.ok) {
onStatus({ state: 'online', message: 'Sidecar Qwen VL online', host, box: 'sidecar', processRunning: true })
return
}
await sleep(2000)
}
throw createError({
statusCode: 503,
statusMessage: 'Started Sidecar Qwen VL but it never answered. The 101 box can take a couple of minutes to load after a cold boot.'
})
}
export function ensureSidecarReady(onStatus: StatusFn = () => undefined) {
const run = sidecarGate.then(() => ensureSidecarUnlocked(onStatus))
sidecarGate = run.then(() => undefined, () => undefined)
return run
}
export async function pickImageEditHost(onStatus: StatusFn = () => undefined): Promise<ImageEditTarget> {
const beast = getBeastImageHost()
if (!beast) {
+5
View File
@@ -1,4 +1,5 @@
import { getSidecarImageHost } from '~/server/utils/imageComfy'
import { ensureSidecarReady } from '~/server/utils/imageComfyLifecycle'
import { buildVisionPromptWorkflow } from '~/server/utils/promptWorkflow'
import { normalizeShotScript } from '~/utils/parseRecommend'
@@ -220,6 +221,10 @@ export async function runPromptRecommend(job: PromptJob, params: {
mode: string
}) {
job.status = 'running'
job.message = 'Checking Sidecar Qwen VL…'
await ensureSidecarReady((status) => {
job.message = status.message
})
job.message = 'Uploading still to Sidecar Qwen VL…'
const imageName = await uploadPromptImage(params.image, job.id)
job.message = 'Qwen VL is reading the still…'