diff --git a/studio/frontend/src/features/images/images-page.tsx b/studio/frontend/src/features/images/images-page.tsx index eacfea5c15..b8d676df3d 100644 --- a/studio/frontend/src/features/images/images-page.tsx +++ b/studio/frontend/src/features/images/images-page.tsx @@ -985,18 +985,30 @@ export function ImagesPage({ active = true }: { active?: boolean }) { }, [images, hasMore, selectedId, quant]); // Refresh the LoRA picker's suggestions when the loaded model (family) changes. A LoRA is - // trained for a specific base family, so a model swap invalidates the current selection -- - // clear it (the user re-adds a suggestion or types a Hub repo id for the new family). We do + // trained for a specific base family, so a real model SWAP invalidates the current selection + // -- clear it then (the user re-adds a suggestion or types a Hub repo id for the new family). + // But do NOT clear on the first load or an unload: a user can restore a saved recipe (which + // sets loras) BEFORE the model finishes loading, and clearing on that load->capable + // transition would silently drop the restored adapters. We track the previously-loaded + // family in a ref and clear only when it changes to a different loaded family. We also do // NOT filter the selection against the discovered catalog: a valid pick can be a free-text // Hugging Face repo id that is not in the (often empty) curated list. const loraCapable = Boolean(status?.loaded && status?.supports_lora); + const prevLoraFamilyRef = useRef(undefined); useEffect(() => { if (!loraCapable) { + // Options are gone with the model, but keep the selection: it may have just been + // restored while the model is (re)loading. It is only SENT when loraCapable, and a + // real family swap below clears it. setAvailableLoras([]); - setLoras([]); return; } - setLoras([]); + const fam = status?.family ?? null; + const prev = prevLoraFamilyRef.current; + if (prev != null && prev !== fam) { + setLoras([]); + } + prevLoraFamilyRef.current = fam; let cancelled = false; listDiffusionLoras(status?.family ?? undefined) .then((list) => { @@ -1564,7 +1576,11 @@ export function ImagesPage({ active = true }: { active?: boolean }) { reference_images: condRefImages, // Drop empty (no id typed yet) and zero-weight rows, and trim hand-typed repo ids, // so the recipe records only adapters that actually applied. Empty -> omit entirely. + // Gate on loraCapable: a restore can leave adapters in state while the loaded model + // does not support LoRA (picker hidden), and sending them would fail generation with + // no visible row to remove. loras: (() => { + if (!loraCapable) return undefined; const active = loras .map((l) => ({ id: l.id.trim(), weight: l.weight })) .filter((l) => l.id && l.weight > 0);