Commit graph

1,344 commits

Author SHA1 Message Date
Daniel Han
6b41567d60 Hide unloadable cached rows, hold the dataset interlock, bound a GIF export
- The cached-model listing tagged any repo with a model_index.json as
  text-to-image, so a community pipeline the image loader's trust rule refuses
  still got a row in the Images picker, and a detected-but-untrusted video repo
  fell through to that same tag. Gate the image tag on the load path's rule and
  hide an untrusted video repo outright.
- A routed diffusion pick only carries a GGUF filename, which is all the chat
  picker has, so a curated single-file artifact arrived with no quant and was
  loaded as a pipeline: from_pretrained on a repo with no model_index.json. Pass
  the page's own catalog spec into the route pick, so a routed pick resolves to
  exactly what a direct pick on that page resolves to.
- The dataset mutation endpoints checked is_active() and only then handed their
  filesystem work to a thread, so a start reserving in that gap changed captions
  or removed images underneath the preflight or the running trainer. The
  interlock is now registered for the whole request under the lock reserve()
  uses, and a start refuses while a mutation is open rather than waiting on it.
- GIF export held every kept frame as a paletted image before encoding; a clip
  may be 2048x2048 for 1024 frames, and at the 12 fps target the step is 1, so
  one export click could allocate over 4 GB and take the backend down. Downscale
  past 720 px and widen the step to keep at most 300 frames.
- seed accepted any Python int, so an out-of-range one passed every preflight,
  evicted the resident models, spawned the trainer and only then died in
  torch.manual_seed. Bound it to torch's 64-bit range in the request and config.
2026-07-26 21:37:01 +00:00
Daniel Han
68912bfaf2 Invalidate latents on a VAE swap, keep a cut-off generation, surface the EMA adapter
- source_revision() scanned the checkpoint root plus text_encoder/tokenizer but
  not vae, so swapping or fine-tuning the VAE in place left the conditioning
  cache namespace unchanged and a warm run trained against latents from the old
  checkpoint. Include the vae directory, like any other component the cached
  tensors come from.
- /images/generate answers only when the images are saved, and secure mode's
  tunnel caps an origin response near 100 seconds, which a native CPU or a
  high-step run passes routinely. The page reported failure while the work kept
  running, and a retry would duplicate it. A lost response (fetch rejection or
  a gateway status the origin never answered) is now told apart from a refusal:
  the page waits out generate-progress and reloads the gallery, so the run it
  started still lands.
- The trainer emits the EMA adapter's path with the terminal event, but the
  state update dropped it, so neither the run history nor either response
  schema carried it and an enabled EMA left nothing discoverable. Keep it, and
  show it next to the primary adapter.
- weighting_scheme advertised a choice of timestep sampling; sampling is always
  logit-normal and the flag only selects the bell loss weights. Describe what
  it does.
2026-07-26 20:47:46 +00:00
Daniel Han
0add1accfd Cancel an evicted safetensors load, spare the arbiter for CPU-only chat, fetch clips lazily
Four fixes from the latest review round:

- The GPU arbiter's chat evictor only cancelled the llama.cpp side. The
  orchestrator publishes active_model_name once its worker reports success, so
  an in-flight safetensors load was visible only as an entry in loading_models
  and finished onto the GPU after ownership had transferred. Cancel every
  pending load, and give the safetensors branch the post-load ownership recheck
  the GGUF branch already had.
- A manual gpu_layers=0 GGUF load runs on the CPU with the GPUs hidden from the
  child, yet it took the arbiter unconditionally: it cancelled a running image
  or video generation for a model needing no VRAM, then held CHAT ownership so
  the next GPU workload unloaded it for nothing. Gate the acquire on the same
  predicate the launch-time CPU-only mask uses, as the image and video loaders
  gate on their resolved device.
- The staged-download hook subscribes per repo, not per job, so another job on
  the same repo advanced the staged queue (starting a load whose scoped files
  were still downloading) or wiped a queue that was still running. Compare the
  variant each callback carries, like the chat page's auto-load does.
- The video gallery fetched every record of a page into an object URL that
  lives until the page closes: 50 clips at tens to hundreds of MB each, for
  cards the user may never scroll to. Fetch a clip as its card nears the strip's
  edge, plus the selected one the player needs.
2026-07-26 19:29:54 +00:00
Daniel Han
1c5d41a5b0 Stage what an LTX-2.3 load reads, keep a routed file's load kind, drop an unbakeable LoRA
Three from the latest review.

The video download plan always asked for the wide base file list, so an LTX-2.3
pick staged the 2.0 base's VAEs, vocoder and connectors that the checkpoint
supplies itself, while the companion files the 2.3 assembly does read were left
out of the plan and pulled inline at load, outside the panel's progress, cancel
and disk preflight. The plan now recognises a 2.3 pick by name (the load keeps
the authoritative header probe, and under-guessing only falls back to the
load-time pull), narrows the base list, and stages the extras in the same entry
as the checkpoint so one repo stays one scoped job.

A pick routed from the chat picker arrives as ?model= and ?quant= with no picker
metadata, so a bare local .gguf or .safetensors was loaded as a pipeline: an
explicit model_kind wins over the backend's filename sniffing, so it evicted the
resident model and then failed on the missing model_index.json. Both pages now
derive the load kind from the path, the same way their own picker handlers do.

A torchao int8/fp8 build takes adapters only at load time. Switching artifact
inside one family keeps the LoRA selection, since the family did not change,
but the load did not bake it, so the next generation was rejected with 'reload
the model with the adapter selection' while the picker still showed the adapter
as active. The selection is now dropped once per resident build, with a message
saying to pick and load again.
2026-07-26 18:19:48 +00:00
Daniel Han
7827679b77 Stop a background page and a stale record taking the GPU or a download with them
Five fixes from a review pass over the diffusion work.

delete-finetuned rmtree'd a model the Images or Video engine was holding: every
guard on that route is chat-only, and Images loads any local path, so deleting a
local diffusion model under the storage root pulled the weights (and the
companion VAE / text encoders sd.cpp re-reads each generation) out from under a
live pipeline. The cached-model route already refuses this; the trained/exported
one now does too, matching by path rather than repo id, and failing open on a
chat-only install so it cannot block ordinary deletes.

A staged download finishing while its page was hidden loaded the model and
evicted whatever the user was actually using: both diffusion pages stay mounted
behind the router and a load takes the GPU unconditionally. The pick is now held
until its page is on screen again, which is also what chat does.

A scoped download could report success having fetched nothing. With Hugging Face
metadata unavailable no manifest is written, so verification is a no-op, and
snapshot_download returns an existing snapshot folder without downloading when
its own repo_info call fails. A repo already on disk from a full snapshot job
(which ignores *.gguf) therefore completed with no weights and auto-loaded
against them. The requested file list needs no network, so it is checked against
the disk directly.

The XET to HTTP retry reclaimed the job slot without the scoped file list, and
that claim overwrites the stored record, so a later identical scoped start
compared an empty list against the real one and 409'd instead of adopting the
running download.

The DiT accelerator gate probed torch.mps.is_available(), which only exists from
torch 2.5 while the supported floor is 2.4. All three probes shared one
try/except, so on torch 2.4 the AttributeError read as 'no block' and a CPU-only
host still evicted the resident pipeline, downloaded the encoders and died in
the child. Each accelerator is probed on its own now, through
torch.backends.mps.
2026-07-26 16:12:27 +00:00
Daniel Han
bc00a8e797 Serialize the GPU handoffs, gate DiT training on a GPU, and keep 3.9 installable
Six review findings, three of them evict-then-fail orderings:

- The chat load reclaimed the GPU without telling the arbiter it existed. A
  chat load holds no llama-server process until its GGUF has downloaded,
  which is minutes, so a competing Images/Video acquire in that window
  found nothing to cancel, took the GPU, and the chat load then spawned
  onto the same device. It now registers an in-flight marker through
  acquire_for's register hook (under the arbiter lock, as the image and
  video loads do), the evictor cancels a marked load, and the route undoes
  itself if ownership moved while it loaded.
- The Hub-download conflict check ran after that handoff, so a GGUF the
  download manager already owns destroyed the resident Images/Video
  pipeline and then 409'd, having loaded nothing. It moves above the
  handoff, together with the marker it handshakes with.
- The image load released the engine router's transition lock before
  registering the load, so a second load choosing the other engine could
  unload the still-idle engine this one captured; the load then landed on a
  deactivated engine, where generate, status, unload and the arbiter's
  evictor can no longer reach it. Registration now happens under that lock
  and refuses if the engine changed.
- Training a DiT family on a host with no GPU was accepted: nf4 is not a
  CPU fallback, its 4-bit load goes through bitsandbytes, which requires
  CUDA, XPU or MPS. The start unloaded the working Images pipeline, pulled
  the text encoders, and only then died in the child. Rejected before the
  teardown now, and /info stops advertising a precision that always 400s.
  SDXL keeps its documented fp32-on-CPU path.
- Both diffusion pages kept the routed-pick marker forever, so re-picking
  the same checkpoint (after chat evicted it) neither loaded nor cleared
  the query string. The marker is released once the query is gone. The
  Images key also carried a stray NUL byte, which made the file read as
  binary to grep and other tooling.
- diffusers dropped Python 3.9 in 0.38, so the unconditional >=0.39.0 pin
  left pip no candidate at all on 3.9 and made every install that composes
  the huggingface extras unresolvable there. The floor is conditional now.

Also fixes tests that were already red on the branch: two hand-built
request fakes had gone stale against fields this branch added, and the
handoff-ordering test only failed on a host with fewer than two GPUs.
2026-07-26 14:46:02 +00:00
Daniel Han
a7d415262a Keep the scoped download key derivable, and stop the hidden page hijacking a route
Four review findings, the first a regression from my own last commit:

- Keying scoped download jobs by a digest of the file set broke the
  download manager: it builds that key client-side (it polls and cancels
  before any response tells it a key), so it watched and cancelled a key
  no worker owned and never fired its ready callback. Keep the derivable
  "@scope" key and refuse the second request instead when a live job on
  the slot is fetching a different file set -- decided inside the
  registry claim, under the lock, so a concurrent claim cannot slip past
  it. The manager records the file set on the job as well, so a sibling
  quant's transfer is not adopted locally either.
- Both diffusion pages read the route query through a loose useSearch and
  both stay mounted once visited, so the hidden one consumed the other's
  ?model=: it navigated back to its own route and tried to load, say, an
  image checkpoint as a video model. Only the visible page consumes it.
- The staged download plan was built without the configured HF token or
  the Advanced values the load itself sends. The token matters most: the
  backend's Hub metadata lookup is best-effort, so a gated base silently
  planned no companion entry and the load pulled those multi-GB files
  inline, outside the manager. The memory/quant controls decide whether
  the base transformer/ shards are needed at all, and the route dropped
  memory_mode, cpu_offload, the prequant path and the LoRA selection
  before asking for the plan.
- The video preview kept playing after leaving the page: the keep-alive
  layout only hides it, and display:none does not pause a media element,
  so a clip the user unmuted kept its audio going over the next page.
  Pause on the active transition and do not auto-replay while hidden.

Also completes the hand-built request bodies in the hub download tests:
the scoped-files field this branch added to the route read as an
AttributeError against them, failing five tests.
2026-07-26 13:36:38 +00:00
Daniel Han
0a3a8f5570 Route the real GGUF filename, keep non-GGUF curated models, key scoped downloads by file set
Five review findings, four of them ways a click did nothing or fetched
the wrong thing:

- A chat pick of a diffusion model routed ggufVariant (a label like
  Q4_K_M) in the search param the target page uses verbatim as the GGUF
  filename, so the load asked for a file that does not exist. Route
  ggufFilename; no filename means a curated non-GGUF pick, loaded as a
  pipeline.
- The task-scoped pickers kept only GGUF repos, so the catalog's bf16,
  bnb-4bit and single-file fp8 artifacts could not be discovered or
  downloaded on the Images and Video pages even though loadSpecFor
  knows how to load them. Keep curated artifacts whatever their format,
  in Recommended and in Hub search.
- Both pages deduplicated routed selections on the model alone, and
  they now stay mounted, so picking the same repo again -- another quant,
  or the same one after chat evicted it -- returned early without
  loading or clearing the query string. Key on model and quant.
- Every scoped image download shared one @diffusion job key regardless
  of the requested files, so switching quant mid-download adopted the
  running job: the UI waited on the first file set, then loaded a file
  that was never fetched. Include a digest of the file set in the key.
- A scoped plan silently dropped requested files missing from Hub
  metadata, and snapshot_download succeeds when an allow pattern matches
  nothing, so the job reported completion and triggered a load with
  required files absent. Fail the job instead.
2026-07-26 12:53:21 +00:00
Daniel Han
f091be2a49 Merge the branch's staged-download work with the main merge 2026-07-26 12:39:40 +00:00
Daniel Han
f7d54a757f Merge origin/main into image-generation
Resolves the app-sidebar conflict: main added Hub and Projects rows
inline while this branch renders the nav from navRows in the order and
pin state set under Settings -> Appearance. Kept the data-driven
rendering, having checked both of main's additions are already
represented there - the projects row carries the same icon, label,
active check, handlers and inline New project button.

Main also replaced the sidebar's inline name field with
NewProjectDialog, which owns its own state, so the button no longer
resets a name draft: it sets the move target and opens the dialog, as
main's other call sites do.
2026-07-26 12:37:00 +00:00
Unsloth
115a50cc30 Route a chat pick of a diffusion model to the Images or Video page
Chat cannot load one, so it was either hidden or failed on load. The unfiltered
picker now lists on-device diffusion models and navigates to the page that runs
them, passing the repo and quant so that page loads it.
2026-07-26 04:56:16 -07:00
Unsloth
4c0a95c7ca Apply the picker task filter to local model sections
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.
2026-07-26 04:56:16 -07:00
Unsloth
cb93f5e4d1 Fetch staged GGUF checkpoints as scoped jobs, and stop calling diffusion models unsupported
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.
2026-07-26 04:56:16 -07:00
Unsloth
84a7f048e5 Stage image and video downloads through the Hub download manager
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.
2026-07-26 04:56:16 -07:00
Unsloth
5138c39d9b Fix GGUF image model picks doing nothing, and pick the train base in the top bar
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.
2026-07-26 04:55:53 -07:00
Daniel Han
e39cc5b2a5
Studio: use the UI font scale tokens in the Agents settings tab (#7462)
The Agents tab landed with three raw px text utilities, so its avatar initials
and the two status pills ignore the UI font size preference and stay fixed while
the rest of the dialog scales.

Swap them for the existing text-ui-11 / text-ui-10 tokens, which is what the rest
of the frontend already uses (149 and 128 call sites respectively).

This is what test_no_raw_pixel_text_utilities guards, so Repo tests (CPU) has been
red on main since the tab was added, and every open PR inherits the failure.

Co-authored-by: danielhanchen <unslothai@gmail.com>
2026-07-26 04:54:00 -07:00
Daniel Han
950da4cba8 Keep curated models listed, guard the video companion repo, pin diffusers
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.
2026-07-26 11:21:50 +00:00
Daniel Han
3ad0ea8419 Send the picked GGUF filename with the quant so diffusion loads fire
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.
2026-07-26 10:17:47 +00:00
Daniel Han
9eed2bdfa3 Fix quantized-load LoRA bake, prequant family exclusions and outpaint canvas
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.
2026-07-26 08:12:01 +00:00
Daniel Han
5411747726 Show the retained failure when a video page mounts after a failed job
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.
2026-07-26 08:00:41 +00:00
Daniel Han
b782b85a13 Fix diffusion dataset 500s, the dropout-1.0 no-op run and the reset base pick
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.
2026-07-26 08:00:34 +00:00
Daniel Han
d819029be2
Studio: reset the reasoning open state when a new stream starts (#7444) 2026-07-26 00:31:00 -07:00
Michael Han
0220104f51
Add Agents settings tab for unsloth start (#7303)
* Add Agents settings tab for unsloth start

Adds a Settings > Agents tab documenting the `unsloth start` command:
quickstart, supported agents with click-to-copy commands, model
selection, common options, remote Studio setup, argument pass-through,
and a dry-run preview. Agent CLIs found on PATH are badged as installed.

Also removes the "New" badge from the System and Chat tabs.

* Use official brand logos for agents, invert Ollama and OpenRouter in dark mode

Claude Code and OpenAI Codex now use the Anthropic and OpenAI logos from
the provider-logos registry; agents without an official asset keep the
monogram tile. Also inverts the Ollama and OpenRouter logos in dark mode
so their monochrome marks stay visible.

* Title Agents tab "Agents (unsloth start)" and move it below Connections

The in-tab header now reads "Agents (unsloth start)" while the sidebar
label stays "Agents". Reorders the tab to sit below Connections.

* Address review: guard PATH detection, fix copy timeout, OS-aware remote snippet

- Only probe agent PATH in the desktop app on a loopback backend, so
  Installed badges are not driven by a remote server's environment.
- Show the "none found" note only when detection actually ran and
  returned empty, not when the call failed.
- Share one copy hook that resets its timeout on rapid clicks and clears
  it on unmount.
- Render the Remote Studio snippet with PowerShell syntax on Windows.
- Note that --no-launch can still load a model when --model is set.
- Drop unused quickstart translation keys.

* Use client OS for remote commands, fix copy a11y and model wording (#7303)

- Pick the remote snippet shell from the client platform, not the server deviceType
- Single-line the model examples so they paste in POSIX, PowerShell and cmd
- Split the pass-through block into independent one-command copies
- Derive detection visibility instead of clearing state in the effect
- Announce copy success to assistive tech
- Correct the quickstart/model copy: bare start uses the loaded model

* Agents tab: flag the Codex row when the loaded model is not GGUF

* Agents tab: target the active Studio server, wrap narrow rows, index the tab's search terms

* Agents tab: build copied commands from the browser-reachable Studio and show the key placeholder

* Agents tab: index the intro for agent-name searches and keep long commands inside the panel

* Agents tab: omit --api-key so the CLI can replay a saved key for the base

* Agents tab: label the indexed heading rows and fall back to the active desktop API base

* Agents tab: name every supported agent in the indexed intro for PR #7303

* Tighten the agents tab comments for PR #7303

* Build the agents tab example commands from the active Studio base for PR #7303

* Take the agent command shell from the Studio host for PR #7303

* Pick the command shell from where the CLI runs for PR #7303

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
2026-07-26 00:09:53 -07:00
Michael Han
671d6dbf69
Settings: match dialog fills to the app shell surfaces (#7457)
* Settings: match dialog fills to the app shell surfaces

Tabs use the sidebar fill and the content pane uses the page fill, so
both track the active palette in light and dark.

* Pair the tab column fill with the sidebar foreground

Custom themes set --foreground but not --sidebar, so search result rows
could land white on white. Track the sidebar token instead.
2026-07-26 00:01:22 -07:00
Michael Han
bac04ab577
Add drag and drop sources to the create project dialog (#7441)
* feat(studio): add drag and drop sources to create project

Files dropped on the create-project dialog upload to the new project's
sources as soon as it exists, so a project can start with context instead
of needing a second trip to the Sources tab.

The sidebar and projects page dialogs now reuse NewProjectDialog rather
than each keeping their own copy, and the OCR / caption ingest overrides
move to a shared helper so every upload path sends the same settings.

* fix(studio): harden project source drops

Drops are not filtered by the `accept` attribute the way the picker is, so a
folder or an image would stage and then fail server-side with a confusing
per-file error. Unsupported entries are now refused up front with one message.

Cancel bypassed the dialog's reset, so a discarded name and its staged files
came back on reopen and uploaded into the next project created. Every close
path now goes through one handler.

Long filenames lost their extension in _sanitize_filename and were then
rejected as an unsupported type; the stem is trimmed instead. Adds backend
tests for the project scope, the sanitizer and path stripping.

* fix(studio): address second review pass on source drops

A drop landing on the panel while uploads run was not cancelled, because
pointer-events-none took the panel out of hit testing and nothing else on the
page cancels a file drop. The browser would navigate to the file and kill the
uploads in flight. Drag defaults are now cancelled even while disabled, and the
files are ignored instead.

Name, size and mtime can match for two genuinely different files, so a skipped
duplicate now says so rather than disappearing.

A slow upload could resolve after the dialog unmounted and still navigate,
pulling the user off the page they had moved to. Post-upload work is gated on
the component still being mounted.

* fix(studio): make source drops safe under StrictMode replay

The mount sentinel was only cleared in effect cleanup, so StrictMode's
setup/cleanup/setup replay left it false for good and every create in a dev
build stopped short of closing the dialog or navigating. It is now set on
setup as well.

The pending-sources marker was consumed inside a useState initializer, which
React replays, so the discarded pass ate the flag and the project opened on
Chats. Reading is now a peek and the marker is dropped in an effect.

Identical bytes under two names collapse to one document server-side, which
looked like both files had been added. The upload loop now tracks returned
document ids and says when files were merged.

* fix(studio): guard the route and storage around staged uploads

The sidebar's dialog lives in the root layout and never unmounts on a route
change, so the mount check alone could not stop a slow upload from navigating
the user back to the new project. The route is captured when create is pressed
and compared afterwards, and callers get that answer so the sidebar can still
move a chat while leaving the user where they are.

Reading the vision-pass overrides went straight at localStorage, which throws
outright where storage is blocked. That happened before the upload loop, so a
project was created and every staged source was lost. It now falls back to the
backend defaults, matching loadOptionalBool in the chat runtime store.
2026-07-25 23:54:48 -07:00
Michael Han
8dffde9611
Sidebar: settings gear above the profile in the collapsed rail (#7458)
The profile-row cog is hidden when the rail collapses, leaving no way to
reach settings without opening the account menu.
2026-07-25 23:09:48 -07:00
Michael Han
0a2a4e2e32
Settings: widen dialog to 960px and raise height to 680px (#7456)
Also caps the height at the viewport instead of pinning it, so short
viewports no longer get a clipped dialog.
2026-07-25 22:46:53 -07:00
Unsloth
5fd086326c Use the ui font-size tokens instead of raw px text utilities
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.
2026-07-25 20:30:08 -07:00
Unsloth
917245e34a Images and Video: tighten code comments 2026-07-25 05:00:49 -07:00
Unsloth
2f2c3fd887 Images Train: a little more spacing between field groups 2026-07-25 04:58:19 -07:00
Unsloth
7610284385 Images Train: shorter copy throughout
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.
2026-07-25 04:50:08 -07:00
Unsloth
c9a42c23bb Images and Video: narrower generation rail, matching Train headings
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.
2026-07-25 04:45:56 -07:00
Unsloth
f813b32220 Video: same treatment as the Images tabs
- 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.
2026-07-25 04:40:53 -07:00
Unsloth
cb22465d85 Images Train: roomier example cards with Import on the thumbnail row 2026-07-25 04:35:34 -07:00
Unsloth
bfe27c27df Images Train: plainer field text, no green buttons, columns that stop colliding
- 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.
2026-07-25 04:22:12 -07:00
Unsloth
7e5f4d87de Images: center the mode switch, flip the arrow with the orientation, app tooltips everywhere
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.
2026-07-25 04:17:05 -07:00
Unsloth
f5e05b4816 Images: restore the top bar position, drop the panes lower under it 2026-07-25 04:09:03 -07:00
Unsloth
6a9d2e7faf Images: put both tabs on the Hub's centered measure
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.
2026-07-25 04:06:32 -07:00
Unsloth
6f5a2fe8a1 Images: full-height panes, wider settings rail, Create/Train offset from the selector
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.
2026-07-25 03:58:24 -07:00
oobabooga
b9d92c41b3
Studio: prevent long reasoning from jumping the chat on completion (#7388) 2026-07-25 03:35:03 -07:00
Unsloth
3f7c2e5f00 Images: flat Create and Train panes, hover-only scrollbars, tidier Train dataset step
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.
2026-07-25 03:13:41 -07:00
shimmyshimmer
e1077798ec Images: one card for controls and preview, chat sliders, wider softer shadow
- 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.
2026-07-25 02:52:27 -07:00
shimmyshimmer
a254dfbcd9 Images: drop card borders for the composer shadow, keep scrollbars inside, use app controls in Train
- 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.
2026-07-25 02:42:46 -07:00
shimmyshimmer
3b5f4549cb Images: one-line workflow rows, stronger trigger fill, roomier mode tabs
- 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.
2026-07-25 02:35:03 -07:00
shimmyshimmer
20aeb9cb0c Images: workflow icons, hint under the trigger, more top room, unclipped Train cards
- 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.
2026-07-25 02:31:04 -07:00
shimmyshimmer
9a6db291c1 Images: make the workflow picker a dropdown instead of a 7-up strip
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.
2026-07-25 02:24:25 -07:00
shimmyshimmer
ee2fbc7764 Images: use the shared pill toggle for Create/Train and pad the panels
- 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.
2026-07-25 02:17:34 -07:00
shimmyshimmer
d933d5289b Studio sidebar: keep the More row highlighted while its panel is open
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.
2026-07-25 02:04:05 -07:00
shimmyshimmer
1cfb516d1b Tighten comments in the new sidebar and delete-guard code 2026-07-25 01:59:03 -07:00
michaelhan
521aafe993 Sidebar settings: hide a lone unpinned tab, match New chat icon, rename Profile menu
- 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.
2026-07-25 01:36:06 -07:00