From 06e40793db356ec874a14a7d18d38b96bb077d62 Mon Sep 17 00:00:00 2001 From: Towsty Date: Fri, 4 Sep 2026 15:52:31 -0500 Subject: [PATCH] Fix LoRA folder drops not appearing without weakening engine filters. Stop empty Comfy blips from freezing the LoRA cache timestamp, and refresh the picker list from Comfy on each poll. Engine tagging (flux/krea) and xaigen platform filtering stay as they were. Co-authored-by: Cursor --- components/SettingsForm.vue | 2 +- pages/index.vue | 12 ++++++++---- server/api/loras.get.ts | 5 +++-- server/utils/loras.ts | 32 +++++++++++++++++++++++++------- 4 files changed, 37 insertions(+), 14 deletions(-) diff --git a/components/SettingsForm.vue b/components/SettingsForm.vue index 6db16a9..643b195 100644 --- a/components/SettingsForm.vue +++ b/components/SettingsForm.vue @@ -760,7 +760,7 @@ async function forceResetGpu() { async function loadLoraKeys() { loraRowsLoading.value = true try { - const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] })) + const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras?fresh=1').catch(() => ({ image: [], video: [] })) const library = await $fetch<{ loraAliases?: Record loraTriggers?: Record diff --git a/pages/index.vue b/pages/index.vue index b054115..7f52ad3 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -3915,17 +3915,21 @@ function rememberLoraNames(key: string, names: string[]) { } async function pollLoras() { - const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] })) + const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras?fresh=1').catch(() => ({ image: [], video: [] })) const nextVideo = listed.video || [] const nextImage = listed.image || [] - const sameNames = (a: string[], b: string[]) => a.length === b.length && a.every((name, i) => name === b[i]) - if (nextVideo.length && !sameNames(videoLoras.value, nextVideo)) { + const sameNameSet = (a: string[], b: string[]) => { + if (a.length !== b.length) return false + const left = new Set(a) + return b.every(name => left.has(name)) + } + if (nextVideo.length && !sameNameSet(videoLoras.value, nextVideo)) { videoLoras.value = nextVideo rememberLoraNames('aigen-lora-names-video', nextVideo) } else if (!videoLoras.value.length) { videoLoras.value = readStoredLoraNames('aigen-lora-names-video') } - if (nextImage.length && !sameNames(imageLoras.value, nextImage)) { + if (nextImage.length && !sameNameSet(imageLoras.value, nextImage)) { imageLoras.value = nextImage rememberLoraNames('aigen-lora-names-image', nextImage) } else if (!imageLoras.value.length) { diff --git a/server/api/loras.get.ts b/server/api/loras.get.ts index 31c1bf1..27e1992 100644 --- a/server/api/loras.get.ts +++ b/server/api/loras.get.ts @@ -1,7 +1,8 @@ import { listStudioLoras } from '~/server/utils/loras' -export default defineEventHandler(async () => { - const listed = await listStudioLoras() +export default defineEventHandler(async (event) => { + const fresh = String(getQuery(event).fresh || '') === '1' + const listed = await listStudioLoras({ fresh }) return { image: listed.image, video: listed.video diff --git a/server/utils/loras.ts b/server/utils/loras.ts index e82e6a7..d79d2cf 100644 --- a/server/utils/loras.ts +++ b/server/utils/loras.ts @@ -142,10 +142,12 @@ async function discoverFromHost(via: 'video' | 'image') { return { image, video } } -async function loadLoraCache(): Promise { +async function loadLoraCache(options: { fresh?: boolean } = {}): Promise { + const fresh = options.fresh === true const now = Date.now() - if (cache && now - cache.at < CACHE_MS) return cache - if (inflight) return inflight + if (!fresh && cache && now - cache.at < CACHE_MS) return cache + if (inflight && (!fresh || inflightFresh)) return inflight + inflightFresh = fresh inflight = (async () => { const videoHost = comfyConfigured() ? getComfyHost() : '' const imageHost = getBeastImageHost() @@ -161,23 +163,39 @@ 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. + // Comfy asleep / unreachable returns empty. Keep the last good list so the picker + // does not vanish — but do NOT refresh `at`, or a blip freezes new folder drops out. if (!next.image.length && !next.video.length && cache && (cache.image.length || cache.video.length)) { - cache = { ...cache, at: Date.now() } return cache } + // Prefer the richer list when a partial reply would shrink a known catalog. + if ( + cache + && (next.image.length + next.video.length) < (cache.image.length + cache.video.length) + && next.image.every(name => cache!.image.includes(name) || cache!.video.includes(name)) + && next.video.every(name => cache!.image.includes(name) || cache!.video.includes(name)) + ) { + const merged: LoraCache = { + at: Date.now(), + image: uniqueNames([...cache.image, ...next.image]), + video: uniqueNames([...cache.video, ...next.video]) + } + cache = merged + return merged + } cache = next return next })().finally(() => { inflight = null + inflightFresh = false }) return inflight } -export async function listStudioLoras() { +export async function listStudioLoras(options: { fresh?: boolean } = {}) { const xaigen = isXaigenStudio() try { - const listed = await loadLoraCache() + const listed = await loadLoraCache(options) return { at: listed.at, image: filterLorasForStudio(listed.image, xaigen),