diff --git a/components/LoraSelect.vue b/components/LoraSelect.vue
index ed42a91..d1518b8 100644
--- a/components/LoraSelect.vue
+++ b/components/LoraSelect.vue
@@ -80,24 +80,23 @@
CLIP unused on this graph — model strength only.
Flux LoRAs stay off on Krea.
+ Flux LoRAs stay on the Flux engine. Switch back to restore that chain.
@@ -3256,10 +3257,37 @@ function applyComfyHealth(health: {
imageSidecarHost.value = String(health?.imageComfy?.sidecar?.host || imageSidecarHost.value || '')
}
+function readStoredLoraNames(key: string) {
+ try {
+ const parsed = JSON.parse(localStorage.getItem(key) || '[]')
+ if (!Array.isArray(parsed)) return [] as string[]
+ return parsed.filter((name): name is string => typeof name === 'string' && Boolean(name.trim()))
+ } catch {
+ return [] as string[]
+ }
+}
+
+function rememberLoraNames(key: string, names: string[]) {
+ if (!names.length) return
+ try { localStorage.setItem(key, JSON.stringify(names)) } catch { /* ignore */ }
+}
+
async function pollLoras() {
const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] }))
- videoLoras.value = listed.video || []
- imageLoras.value = listed.image || []
+ const nextVideo = listed.video || []
+ const nextImage = listed.image || []
+ if (nextVideo.length) {
+ videoLoras.value = nextVideo
+ rememberLoraNames('aigen-lora-names-video', nextVideo)
+ } else if (!videoLoras.value.length) {
+ videoLoras.value = readStoredLoraNames('aigen-lora-names-video')
+ }
+ if (nextImage.length) {
+ imageLoras.value = nextImage
+ rememberLoraNames('aigen-lora-names-image', nextImage)
+ } else if (!imageLoras.value.length) {
+ imageLoras.value = readStoredLoraNames('aigen-lora-names-image')
+ }
}
async function pollComfyHealth() {
@@ -3315,7 +3343,9 @@ onMounted(async () => {
} catch { /* ignore */ }
videoLoraStack.value = readStoredLoraStack('aigen-video-lora')
imageLoraStack.value = readStoredLoraStack('aigen-image-lora')
- imageV2LoraStack.value = readStoredLoraStack('aigen-image-v2-lora')
+ videoLoras.value = readStoredLoraNames('aigen-lora-names-video')
+ imageLoras.value = readStoredLoraNames('aigen-lora-names-image')
+ imageV2LoraStack.value = readImageV2LoraStack(v2Engine.value)
seedImageV2LorasIfEmpty()
globalLocks.value = localStorage.getItem(LOCKS_STORE) || ''
try {
@@ -3482,7 +3512,7 @@ watch(imageLoraStack, (value) => {
}, { deep: true })
watch(imageV2LoraStack, (value) => {
- try { localStorage.setItem('aigen-image-v2-lora', JSON.stringify(value)) } catch { /* ignore */ }
+ persistImageV2LoraStack(value, v2Engine.value)
}, { deep: true })
function clampDuration(value: number) {
@@ -3536,6 +3566,26 @@ function readStoredLoraStack(key: string) {
}
}
+function imageV2LoraStoreKey(engine: ImageV2Engine) {
+ return `aigen-image-v2-lora-${engine}`
+}
+
+function persistImageV2LoraStack(stack: LoraStackItem[], engine: ImageV2Engine) {
+ try {
+ const payload = JSON.stringify(stack)
+ localStorage.setItem(imageV2LoraStoreKey(engine), payload)
+ localStorage.setItem('aigen-image-v2-lora', payload)
+ } catch { /* ignore */ }
+}
+
+function readImageV2LoraStack(engine: ImageV2Engine) {
+ const keyed = readStoredLoraStack(imageV2LoraStoreKey(engine))
+ if (keyed.length) return filterLoraStackForImageEngine(keyed, engine)
+ const legacy = readStoredLoraStack('aigen-image-v2-lora')
+ if (!legacy.length) return [] as LoraStackItem[]
+ return filterLoraStackForImageEngine(legacy, engine)
+}
+
function mergeLoraOptions(listed: string[], extra: string[]) {
const names = [...listed]
for (const name of extra) {
@@ -3718,6 +3768,7 @@ function applyGenerationPreset(preset: GenerationPreset) {
const skipped = preset.loraStack.length - stack.length
if (preset.kind === 'imagev2') {
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'
v2Engine.value = parseImageV2Engine(settings.engine, 'flux')
v2Task.value = v2Mode.value === 'compose' ? (settings.task || 'scene') : v2Mode.value === 'refine' ? 'refine' : v2Mode.value === 'generate' ? 't2i' : 'scene'
@@ -5006,6 +5057,7 @@ function applyStillSettings(still: LibraryStill) {
? settings.mode
: 'edit'
setV2Mode(mode)
+ persistImageV2LoraStack(imageV2LoraStack.value, v2Engine.value)
v2Engine.value = parseImageV2Engine(settings?.engine, 'flux')
if (!settings) return
if (typeof settings.negative === 'string') v2Negative.value = settings.negative
@@ -5638,19 +5690,19 @@ function setV2Mode(mode: ImageV2Mode) {
}
function setV2Engine(engine: ImageV2Engine) {
+ if (engine === v2Engine.value) return
const previous = v2Engine.value
+ const previousStack = imageV2LoraStack.value
+ persistImageV2LoraStack(previousStack, previous)
v2Engine.value = engine
+ imageV2LoraStack.value = readImageV2LoraStack(engine)
+ seedImageV2LorasIfEmpty()
if (engine === 'krea') {
v2Steps.value = IMAGE_V2_KREA_STEPS
v2Cfg.value = IMAGE_V2_KREA_CFG
v2Turbo.value = false
- const dropped = imageV2LoraStack.value.filter(item => isKleinImageLora(item.name))
- if (dropped.length) {
- imageV2LoraStack.value = filterLoraStackForImageEngine(imageV2LoraStack.value, 'krea')
- seedImageV2LorasIfEmpty()
- toast(dropped.length === 1
- ? kleinLoraOnKreaMessage(dropped[0].name)
- : 'Flux LoRAs were removed. They cannot be used with Krea.')
+ if (previousStack.some(item => isKleinImageLora(item.name))) {
+ toast('Flux LoRAs stay on the Flux engine. Switch back to restore that chain.')
}
if (v2Mode.value === 'edit') v2Denoise.value = IMAGE_V2_KREA_EDIT_DENOISE
if (v2Mode.value === 'compose') v2Denoise.value = IMAGE_V2_KREA_COMPOSE_DENOISE
@@ -5659,7 +5711,6 @@ function setV2Engine(engine: ImageV2Engine) {
if (previous === 'krea') {
v2Steps.value = IMAGE_V2_STEPS_DEFAULT
v2Cfg.value = IMAGE_V2_CFG_DEFAULT
- seedImageV2LorasIfEmpty()
}
}
diff --git a/server/utils/loras.ts b/server/utils/loras.ts
index 3ee9944..221b092 100644
--- a/server/utils/loras.ts
+++ b/server/utils/loras.ts
@@ -161,6 +161,11 @@ async function loadLoraCache(): Promise {
image: uniqueNames([...image.image, ...video.image]),
video: uniqueNames([...video.video, ...image.video])
}
+ // Comfy asleep / unreachable returns empty. Keep the last good list so the picker does not vanish.
+ if (!next.image.length && !next.video.length && cache && (cache.image.length || cache.video.length)) {
+ cache = { ...cache, at: Date.now() }
+ return cache
+ }
cache = next
return next
})().finally(() => {
@@ -170,15 +175,22 @@ async function loadLoraCache(): Promise {
}
export async function listStudioLoras() {
+ const xaigen = isXaigenStudio()
try {
const listed = await loadLoraCache()
- const xaigen = isXaigenStudio()
return {
at: listed.at,
image: filterLorasForStudio(listed.image, xaigen),
video: filterLorasForStudio(listed.video, xaigen)
}
} catch {
+ if (cache) {
+ return {
+ at: cache.at,
+ image: filterLorasForStudio(cache.image, xaigen),
+ video: filterLorasForStudio(cache.video, xaigen)
+ }
+ }
return { at: 0, image: [] as string[], video: [] as string[] }
}
}