LM Studio, ./models and custom-folder rows ignored it, so the Images picker listed
chat GGUFs that 400 on a diffusion load. The backend already tags every local model
with a task for this purpose.
A GGUF entry went out as a full snapshot, whose ignore list drops *.gguf: the job
finished at once having fetched only docs, and the repo landed on device unloadable.
Every entry is scoped now. The Hub also no longer tags image/video models as
unsupported (they run on their own pages), and those pickers name what they select.
They downloaded inline inside the load, so they had none of the manager's disk
preflight, manifest verification, resume or panel progress. Picks now stage as
scoped jobs carrying the loader's own file list, then load from a warm cache.
Lets a consumer that reads a deliberate subset of a repo stage it through the
normal download manager. Keyed as "@scope" so it never collides with a quant or
with the repo's full snapshot, and the file list rides the registry so an
XET to HTTP retry respawns the same scoped job.
Reports the repos and exact files a pick needs so the download manager can stage
them with the loader's own file scope. A plain snapshot would add the packaged
root single, transformer shards and fp16 twins the loader never opens.
Both read huggingface_hub's import-time HF_HUB_CACHE, which changing the cache
folder does not update: progress counted the old root while the download wrote to
the new one, and from_pretrained could split one model across both.
The quant rows never forwarded the .gguf filename, so every hub GGUF pick on
Images/Video fell through to a silent return. On Train the top bar now picks the
training base instead of a generation model, which is GGUF-only and untrainable.
- The trainer keyed its persistent conditioning cache on family and
resolution only, while the keys themselves carry just the caption or
image content and crop variant. One cache directory reused for two
checkpoints, or for the same repo at a new revision, let a warm run
skip loading its encoders and train on the other model's embeddings
and latent statistics. Namespace on the base checkpoint and its
resolved revision as well. The revision helper now lives beside the
cache in diffusion_train_extras and the inference wrapper delegates to
it, so the two cannot disagree about what counts as the same source.
- The diffusion learning rate only checked positivity, but 1e309 floats
to inf and satisfies gt, so the route evicted the resident models and
started AdamW with an infinite rate: the first step destroys the
adapter while progress looks normal and the result is saved. Bound it
below 1.0, matching the LLM schema, which rejects inf for the same
reason.
Three review findings:
- The picker filtered every catalog member out of Recommended and Hub
search on the way to canonical group rows, but nothing renders those
rows yet (catalogGroupFitsDevice and groupMatchesQuery are imported and
unused). A task-scoped picker's models list is catalogToModelOptions(),
i.e. group members exclusively, so both lists came back empty and no
curated model could be discovered or downloaded. Keep the artifacts
listed until the grouped UI exists.
- The video delete guard compared only repo_id, so deleting the
companion base of a loaded GGUF video model was allowed even though it
supplies the VAE and text encoders. Compare base_repo too, matching
what the images guard already does for its companions.
- diffusers was declared unversioned while the diffusion stack requires
0.39 (Krea2Pipeline, the cache_context child registries, the Flux2 and
Z-Image pipelines), so an upgrade could keep an older release and
selecting an advertised model failed until the user upgraded by hand.
Two correctness fixes:
- The cache keyed the checkpoint and its companion base by name only, so
a Hub repo advancing to a new commit, or a local directory updated in
place, kept returning embeddings from the previous text encoder. Pair
both with a revision marker: the locally resolved commit sha for a Hub
repo, config plus text-encoder file stats for a directory. Neither
loads the encoders, so a warm run still keeps them off the GPU.
- flow_shift only checked positivity, but JSON accepts 1e309, which
floats to inf, and inf <= 0 is False while NaN fails every comparison.
The sigma table then evaluates s * u / (1 + (s - 1) * u) as NaN, which
poisons every sampled sigma and saves a corrupted adapter while
progress looks normal. Require a finite value.
The variant expander emitted only the quant label, and nothing else in
the frontend set ggufFilename, so the Images and Video pages could never
take their GGUF branch: both gate it on meta.ggufVariant and
meta.ggufFilename, then fall through to the single-file path, which
returns because the id is a repo id and not a .gguf name. Every quant
pick was a silent dead click, with no load request reaching the backend.
The filename was already on the variant row (the picker keys its list on
it, and the variant validator requires a non-empty string), so thread it
through the click handler. The chat path is unaffected: it reads
ggufVariant and never needed the filename.
Six review findings across the Images page and model scanning:
- The quantized (int8/fp8) load path can only attach LoRA adapters
before quantization, but the frontend load request had no loras field,
so every generation after such a load was rejected and each reload
repeated it. Send the selection with the load.
- build_prequant_checkpoint passed no family to the scheme exclusions
while recording the family in metadata, so a Qwen int8 artifact baked
the short-M text-stream linears and was then rejected wholesale by the
loader's family-keyed check.
- Registering a bare single-file checkpoint directory produced no On
Device row even though the images loader can load it; only its parent
worked. Admit that shape when nothing else matched.
- Unload left the Reapply target set, so the repair path was skipped and
Reapply reloaded the ejected model. Clear it, as the video page does.
- Both FLUX.2 bases were trusted for training but not inference, so
Deploy to Create rejected every FLUX.2 adapter.
- Outpaint allocated the grown canvas before downscaling, exceeding the
browser canvas area cap on a large photo; an over-cap canvas is
unusable, so Extend silently posted a fully transparent image and
mask. Scale the source first.
Four bugs in the batched inference path, all found by review:
- A mixed-prompt batch sent a scalar negative prompt against a prompt
list. Z-Image asserts on the length, and Qwen-Image, Krea 2 and FLUX
true-CFG encode a batch-1 negative against batch-N latents and fail in
the transformer's text/image concat. Broadcast it to match the batch.
- The FBCache step-cache reset sat above the chunk loop. diffusers only
resets that state at the end of a successful call, so a forward that
raised (the OOM the backoff is meant to recover) left its own residual
behind and the halved retry died on a shape mismatch. Reset before
every forward instead.
- The conditioning cache keyed on the checkpoint alone, but a GGUF or
single-file load takes its text encoders from the companion base, so
the same checkpoint against a different base reused the previous
base's embeddings. Key the base too.
- Gallery records stored the base seed and the requested batch size even
when a prompts/seeds list drove the run, so restoring the second image
of seeds=[5, 99] replayed seed 5. List-driven outputs now record as
single-image recipes on their own seed.
Also bound strength above 0: every img2img pipeline derives its step
count from it, so 0 leaves zero denoising steps and either raises or, on
SDXL, crashes on empty latents.
Mount-time recovery handled only phase=completed, so reloading the page
after a multi-minute generation failed left an idle view with no
diagnosis: the backend keeps the terminal failed record only until the
next job, and nothing else survives the reload. Surface it the same way
the poll does, filtering the cancelled sentinel.
Four correctness fixes on the training side:
- The labeling grid read caption sidecars under except OSError, but a
non-UTF-8 sidecar raises UnicodeDecodeError (a ValueError), so one bad
file 500d /diffusion/dataset/{name}/images and the grid could not be
opened to repair it. Read it as no caption, matching the info summary.
- An image past Pillow's own hard limit raises DecompressionBombError,
which derives straight from Exception and so escaped the upload guard's
(OSError, UnidentifiedImageError, ValueError) and returned 500 instead
of the intended 400.
- lora_dropout accepted 1.0, which makes PEFT build nn.Dropout(p=1.0):
lora_A and lora_B receive no gradient and the run saves an untrained
adapter while reporting normal progress. Bound it below 1.0, matching
the LLM request schema.
- The train panel re-seeded the base repo on every dataset refresh
because the family object identity changes on each info fetch, so an
upload or caption save silently replaced the user's chosen base and the
run started on a different model. Track the pick and only re-seed on a
real family change.
text-[11px] and friends ignore the UI font size preference, which the repo's
font-scale contract test enforces. Same rendered size at the default scale.
read_text raises UnicodeDecodeError, which is not an OSError, so one bad caption
sidecar or video sidecar 500d the info, upload and gallery routes. A flat image
now yields the all-black edge map instead of its own luminance, and the four DiT
loss knobs the trainer implements are declared so model_dump keeps them.
str(None) stored the literal "None" as the caption, so a null row counted as
captioned and would have trained on that text. Also drops an unused import.
- start_training forwards resume_source_run_id to _start_training_impl, which
reads it. Without it every start raised NameError.
- Restore main's anchor in the load-marker order test: the file now has an
earlier `if config.is_gguf:`, so indexing the first one compared the wrong
branch.
- The two diffusion tests that reach diffusers now skip when it is absent,
matching the CPU repo-test env.
- The UI smoke finds nav rows that live in the sidebar's More flyout.
Family notes, example descriptions, precision labels and every helper line are
trimmed so they stop wrapping to three lines and colliding with the next
column. The nf4 label now fits its select without truncating.
Create and Video rails go from 392px to 368px. Train a LoRA and Training
settings are now the same size and both in the heading font: the h2 already
picks it up from the base rule, so the settings header opts in with
font-heading and the weight that rule pins.
- No cards: the rail and the canvas sit on the page background, divided by a
rule that runs the full page height, on the Hub's centered measure.
- Wider rail, chat's sliders, hover-only scrollbars.
- Every native title tooltip is now the app's tooltip, including the clip cards.
- Reapply and Cancel are outline buttons, the empty state uses the Video nav
icon, and the clip tiles are less rounded.
- The dataset name, trigger prompt, adapter name and custom base fields now say
what they are in plain words instead of leaning on example values.
- Import, Upload, Back, Back to settings and Train another are outline buttons,
not green ones.
- Example thumbnails are landscape tiles, so photos are not cropped to chunky
squares.
- Settings cells get min-w-0 and the select value truncates, so a long option
like the nf4 label no longer widens its column into the next one.
- The number stepper sits a little further in from the field edge.
- Create and Train are wider.
The Create/Train switch is centered on the page instead of trailing the model
selector, with wider buttons. The flip control's arrows now rotate with the
orientation and its label says which way the flip goes. Every native title
tooltip on the page is now the app's tooltip, so they all get the rounded
surface instead of the OS box.
Top bar and content now share mx-auto max-w-1100 with px-5 / sm:px-8, so Create
and Train sit at the same width and position as the Hub instead of running edge
to edge.
The rule between the panes now runs the whole page height (the row drops its
bottom padding and each pane pads its own content), the settings rail is wider
on both Create and Train, and the Create/Train switch sits further right of the
model selector.
Both Images tabs now sit on the page background like the Hub: no card, no
shadow, no bounding box. A single rule divides the controls rail from the
preview canvas (Create) and from the run area (Train), and the settings and
previous-runs sections read as panes rather than nested cards.
Also:
- Scrollbars in these panes use the existing hover-scrollbar recipe, so the
thumb only shows while the pane is hovered.
- Workflow rows explain themselves with a tooltip after a short hover, which
also works on disabled rows, and the descriptions are much shorter.
- Training images rows are name plus image count; the license stays on the
example card.
- The upload step loses its dashed box, the buttons match the sizes around
them, and Upload only appears once files are picked.
- The empty preview uses the same icon as the Images nav item.
- Controls and preview were two floating cards; they now share one card split by
a divider. The Advanced dock stays separate since it toggles.
- SliderField wraps Chat's ParamSlider, so the sliders match Chat (label row with
the value, full-width neutral track) instead of a green track with a spin box.
All 14 call sites keep their props.
- panel-soft-surface goes from 0 2px 8px -2px /0.16 to 0 4px 22px -6px /0.10:
lighter, spread wider.
- Cards lose ring-1 for .panel-soft-surface: the composer's shadow in light, flat
in dark, matching .chat-composer-surface and the menus.
- Both rails now clip (overflow-hidden) with the scroller inside, so the scrollbar
can't ride over the rounded corner. Same shape video-page already uses.
- Train's 9 native selects become the app Select, so they no longer open an
OS-native menu, and the native file input is hidden behind a Choose images
button that reports the count.
- Image previews use explicit 8-10px radii: this theme sets --radius to 1.1rem,
so rounded-md was 15.6px and the thumbnails read as circles.
- Dropdown rows are icon plus name only. The selected row's description already
shows under the trigger, and a disabled row keeps its reason as a title.
- Trigger fill moves to the bg-foreground/[0.07] dark:bg-foreground/[0.12] pair
the hub cards use, so it reads against the card in both modes.
- Description under the trigger goes from text-ui-10 to text-ui-11p5.
- More horizontal padding on the Create / Train tabs.
- Each workflow carries an icon, shown on the closed trigger and on every row.
- The trigger is one line (icon plus name). The selected workflow's description
moved below it, where it reads like the Field hints further down the rail.
- pt-6 instead of pt-3 on both Create and Train, so the cards clear the model
selector row.
- The Train right column scrolls while its cards use ring-1, which draws outside
the box and was clipped at the scroll edges. p-px gives the ring room.
Seven workflows in a 340px rail left ~48px each, so the labels crowded and the
hints were only reachable as title tooltips. The strip is now a dropdown: the
trigger shows the current workflow and its hint, and each row carries its own
description. A row the loaded model can't run is disabled and shows the reason in
place of the hint, so the gating explains itself. Adding a workflow no longer
shrinks the others.
- Create/Train was the only segmented control on its own Tabs styling. It now
uses PillTabs, the same control as the model picker and Hub toggles, pinned to
the header row's 34px. PillTabs takes an icon per tab, so the inline-span
workaround for TabsTrigger goes away.
- pt-3 on both the Create and Train panels, which sat flush against the model
selector row.
Moving the pointer into the flyout left the row unhighlighted while the panel
stayed open. The row now carries data-menu-open, added to the nav hover selector
list. Not data-state: the tooltip and menu triggers both write that attribute, so
whichever lands last wins.
- With exactly one tab unpinned, both More and that tab are dropped, so nothing
is drawn for it (previously it rendered inline). The page stays reachable by URL.
- The customizer's New chat preview uses PencilEdit02Icon, the icon the real row
renders; Edit03Icon was a different glyph.
- "Sidebar menu" is now "Profile menu", described as the shortcuts behind your
name at the bottom of the sidebar, so it no longer reads as a second name for
the navigation section above it.
- Search is reached from the top bar, so it is no longer previewed as a fixed
sidebar nav row; New chat stays.
- More now appears only when it would hold two or more rows. A single unpinned
row renders inline in its saved order position instead: a flyout wrapping one
item costs a click and earns nothing. The customizer's More preview follows the
same threshold.
Adds a "Sidebar navigation" section to Settings -> Appearance, above the
existing profile-menu customizer, with the same drag-to-reorder + switch UI.
- New sidebarNav preference: one { id, pinned } entry per navigable row
(projects, hub, images, train, video, recipes, export), array order = render
order. Defaults match the shipped layout, so an untouched install is unchanged.
- Unpinning moves a row into the More flyout rather than hiding it, so no page
becomes unreachable. New chat and Search stay fixed as actions.
- app-sidebar now renders from one navRows descriptor map, so a pinned row and
its flyout counterpart cannot drift; the More row appears only when something
is unpinned and highlights off whatever it actually holds.
- Mirrored in the backend PersonalizationCustomization: without it the model's
extra="ignore" would drop the field, and because sync replaces local state
with the server's copy once customization is saved, the user's pin order would
reset on the next sync. The validator dedupes and back-fills like sidebarMenu
but preserves the client's order, since here order is meaningful.
Frontend typecheck, i18n parity and catalog checks pass; 32 personalization
tests pass, including a round-trip asserting a reordered list survives a save.
- More flyout rows dropped their sidebar-row typography and size-icon override,
which fought DropdownMenuItem's own scale (text-sm, gap-2.5, px-3 py-2 and
size-4 icons) and rendered oversized glyphs and text next to the nav.
- New pill reuses the brand "beta" badge recipe (nav-badge font, --ui-font-scale
sizing, nav token colours) rather than hardcoded 9px values.
- The More row's native title tooltip (an OS box on hover) is replaced by the
app's Tooltip, wrapped around DropdownMenuTrigger so both triggers compose onto
the same button, and shown only on the collapsed rail like other nav rows.
- Images uses Image03Icon and Video uses FlimSlateIcon.
- New "More" row (MoreHorizontalIcon) opens a right-side flyout on click or
hover holding Video, Recipes and Export; the close is delayed 180ms so the
pointer can cross the gap. Its SidebarMenuButton deliberately takes `title`
rather than `tooltip`: with `tooltip` the button returns a Tooltip root and
DropdownMenuTrigger asChild would hand its ref to a non-DOM node.
- Dropped the "Train" section heading; Train is now a top-level row between
Images and More. data-tour="navbar" moves to the surviving nav group so the
product tour keeps its anchor.
- "New" pill beside Images and (inside the flyout) Video, via NavBadge.
Resolve the drift between PR #6763 and current main:
- deletion: main moved cached-model deletion into hub/services/models/deletion.py,
so the PR's Images/Video in-use guards move there too as _diffusion_blocks_delete
and _video_blocks_delete, keeping main's fail-closed 503 contract.
- llama_keepwarm: take main's rewrite, re-apply the PR's image/video inference
suffixes so a generation in flight blocks an idle unload.
- routes/training: keep main's sidecar-swap 409 and resume_source_run_id, run
start_training in the worker thread the PR's unload hook needs.
- model picker: main renamed components/assistant-ui/model-selector ->
features/model-picker/... and rewrote pickers.tsx, so the PR's picker work is
ported onto main's version (task/catalog props, task gating of hub + cached +
local rows, single-device expanderGpuGb, fine-tuned section hidden when scoped)
rather than reverting main's pinned-models and per-model-config work.
- images/video pages: imports repointed at the new model-selector path.
- tests: delete-guard tests retargeted at the deletion service.
Typecheck, i18n parity and model-catalog checks pass.
* fix(studio): honor run settings on initial model load
When loading a model from the gear-icon run-settings page, Context Length
and KV Cache Dtype were ignored if the user clicked Load before blurring
the context field, or before React flushed staged config into the store.
- Add NumericValueInput.commit() to flush a focused draft on Load
- Pass effectiveLoadConfig from model-config-page to onRun
- Prefer selection.config in performLoad for all load knobs
- Preserve meta.forceReload from the config-page reload path
Fixes#7346
* fix(studio): flush NumericValueInput draft when Load blurs first
Clicking Load blurs the context field before handleRun runs, so commit()
returned the stale value prop. Keep draft in a ref and parse it even when
the input is no longer focused.
* fix(studio): preserve Auto context when Load is clicked without edits
NumericValueInput.commit() now returns null unless the user actually
changed the field, so GGUF Load/Save no longer pins the displayed native
context into customContextLength when Auto was left untouched.
* fix(studio): clear NumericValueInput dirty state after blur commit
After a normal blur commit, reset dirtyRef so a later Load cannot replay a
stale draftRef when the user changed context via Reset or the slider.
* test(studio): pin NumericValueInput Auto/dirty contracts for #7346
Lock Codex P1/P2: commit returns null unless dirty, blur clears dirtyRef,
and handleRun only promotes a non-null committed context.
* fix(studio): keep same-click context draft after blur (#7346)
Blur can commit and clear dirtyRef before Load's onClick; stash that
committed value for one imperative commit() so typed context is not lost.
* chore: refresh PR head for #7351
* fix(studio): handle context commit edge cases
* chore: refresh PR head
* test(studio): guard invalid context drafts
* style(studio): format context draft guard
* test(studio): exercise same-click model config loads
* fix(studio): drop stale blur pin when the typed context equals the shown value
NumericValueInput cached every blur commit in lastBlurCommittedRef, even when
the draft equalled the current value and no onChange was dispatched. Because the
displayed value never changed, the useEffect([value]) clear never fired, so a
later Reset or external edit that leaves the shown value unchanged could not drop
the cache and the next commit() replayed it into an override that Reset had
removed. Only cache the blur result when it actually dispatched onChange
(final !== value); when final === value the parent is already current and there
is nothing to bridge. Add a Playwright regression that re-types the shown context
and asserts no override is stored.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
* Studio: commit every same-click numeric draft before staging the load config
The run-settings Load/Reload button flushed only the GGUF Context Length draft
imperatively before building the load config. Max Seq Length (non-GGUF), GPU
Layers and MoE Layers on CPU (GGUF) are the same NumericValueInput and stage
their typed value only on blur, so editing one and clicking Load in the same
gesture staged the load from a still-stale parent config and dropped the value
the user just typed.
Wire an imperative commit handle through those inputs too and fold every
committed draft into the effective config, recomputing the non-GGUF load-time
max sequence length from the committed draft.
* fix(studio): recompute fixed-layer context pin and drop stale blur cache on every render
Two run-settings edge cases on the model-config page:
1) pinFixedLayerContext was computed from the render-time config, before a
same-click GPU Layers draft is committed in handleRun. Typing a positive
fixed-layer value on an auto-fit GGUF and clicking Reload therefore built
the runtime config with customContextLength: null, so a later fresh load
sent the native context with fixed layers (the OOM the pin exists to
avoid). Recompute the pin from the committed effectiveConfig.
2) NumericValueInput cleared its blur bridge only on a value change. A real
edit (final !== value) that Reset then reverts to the same shown number
nets value back unchanged, so the effect never re-ran and the stale pin
survived into the next Load/Save, replaying the override Reset removed.
The bridge is only valid across the single synchronous same-click gesture
that set it, so clear it on every settled render instead.
Add source-contract regressions for both.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: Unsloth <michaelhan@Michaels-MacBook-Pro.local>
Co-authored-by: Michael Han <107991372+shimmyshimmer@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
The gguf order fix that landed on main dropped the only assertion
covering the prerequisite that llama_extra_args inheritance runs before
the GGUF branch: the inherited value (a carried --no-mmproj) shapes the
hub guard's require_mmproj, so a future reorder could reject a load
over an mmproj download the inherited arguments would disable. The
comment also misattributed the inheritance site to
_guard_chat_load_against_training.
The assertion is restored anchored on the call form
"= _resolve_inherited_extra_args(", which pins the endpoint's call site
(the bare name would match the function definition, which always
precedes the endpoint, making the check vacuous), and the comment now
names the real inheritance site. 32 tests pass.
* Studio desktop: fix loading-toast overlap and typing lag on model load
- Toaster: on desktop, offset toasts below the ~34px custom window titlebar
(top 46 when isTauri) so they no longer cover the min/max/close controls.
Web is unchanged (top 12).
- Model load: the 2s load poll wrote loadProgress state every tick, which
re-renders the whole chat page during "Starting model" (cheap in Chrome,
janky in the desktop WebView2 -> laggy typing). That state is only read by
the dismissed-toast inline status, so gate all four poll branches to write
it only when the inline view is live; while the toast is up it updates via
Sonner alone.
* Studio desktop: fix HTML canvas preview, download, and panel offset
- CSP: add frame-src for localhost/127.0.0.1 so the desktop webview can
frame the backend-served artifact preview. default-src 'self' (no
frame-src) blocked it -> "127.0.0.1 refused to connect"; web is
same-origin so it already worked.
- Download: route the canvas Download button through the native save
dialog (downloadFile) instead of a blob-anchor click, which the Tauri
WebView2 silently drops.
- Nudge the artifact panel down 8px so its top edge/shadow isn't tucked
under the window top bar.
* Studio desktop: add HTML filter for native canvas save dialog
Canvas Download saves .html via save_native_file, but save_filter() had no
html/htm case, so the native dialog fell back to the JSON/CSV/etc filter and
could block saving/browsing the .html export. Add an HTML filter and include
html/htm in the catch-all. Addresses Codex review on #7391.
* Studio desktop: unblock canvas preview in dev shell + clear header fade
- Preview: the app CSP frame-src fix wasn't enough in the tauri dev shell.
The preview endpoint sets its own frame-ancestors response header, which
only allowed 'self' tauri://localhost http://tauri.localhost -- so the
Vite dev origin (http://localhost:5173) was blocked and the frame stayed
"refused to connect". Extend the allowlist with http://localhost:* and
http://127.0.0.1:* (the endpoint only renders postMessage'd HTML in a
no-same-origin sandbox, so it exposes no server resource).
- Shadow: the artifact panel toolbar sat under the full-width
chat-header-fade; lower the panel top (mt 80->90px) so the controls clear
the fade.
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>