Commit graph

5022 commits

Author SHA1 Message Date
Maheswar Kumar
5f38afbc2d
studio: restore file drops and composer drag feedback (#13387)
Some checks are pending
Mac Unsloth Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Unsloth Tauri CI / Rust unit tests (macos) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (extra) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (chat) (push) Waiting to run
Unsloth UI CI / Loaded-models indicator (cross-browser) (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Studio VAE seam guard / seam-guard (push) Waiting to run
Mac Unsloth Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Mac Unsloth UI + API + Update + Inference CI / Chat UI, API, Update and GGUF inference (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Unsloth Tauri CI / Rust unit tests (windows) (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (macOS) (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (Windows) (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection + VC++ round-trip (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 units, no-VS resolve, VC++ round-trip (windows-latest) (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
studiobench CI / Selftests, lint and the single-file build (push) Waiting to run
studiobench CI / Real-path session, every action ran (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
Windows Application Control CI / installer survives a denied unsloth.exe (push) Waiting to run
Windows ARM64 CI / woa (pwsh-7) (push) Waiting to run
Windows ARM64 CI / woa (windows-powershell-5.1) (push) Waiting to run
Windows No Compiler CI / the installer spawns no C# compiler (push) Waiting to run
Workflow trigger lint / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Workflow trigger lint / security suites (torch-dependent) (push) Waiting to run
* studio: restore native file drops to the main webview

* studio: attach dropped documents to the composer and reuse drag feedback

* studio: remove unused attachment badge state

* studio: tidy native drag context import and stale drop comment

Import NativeAttachmentDragContext from the native-intents barrel like its siblings, indent the new provider in ChatPage, and update the OFFICE_ATTACHMENT_EXTS doc comment, which still pointed at the removed OFFICE_DOC_EXTS.

---------

Co-authored-by: mahiatlinux <mahiatlinux@users.noreply.github.com>
Co-authored-by: Michael Han <107991372+shimmyshimmer@users.noreply.github.com>
2026-10-12 15:43:03 -07:00
Daniel Han
e4609246eb
Studio: run LTX-2.5 on the Video page (#13463) 2026-10-12 13:33:41 -07:00
Daniel Han
88ca164e65
Format two test files that #13378 and #13437 left off the formatter's fixed point (#13462)
test_formatter_fixed_point fails on main for studio/backend/tests/test_openai_videos_route.py (#13378) and tests/studio/playwright_find_in_page.py (#13437). Output of scripts/run_ruff_format.py, no behaviour change.
2026-10-12 10:31:42 -07:00
Daniel Han
c6db48dd38
Codex device-poll tests: fake asyncio.sleep only on the test's own thread (#13457)
codex_auth.asyncio is the asyncio module, so patching its sleep replaced asyncio.sleep for every event loop in the worker. A loop left running by an earlier test then spun on the instant fake and flooded the recorded sleeps, and the assert's repr of that list ran past the 330 s pytest-timeout (seen on #13445). Route other threads to the real sleep.
2026-10-12 09:31:30 -07:00
Daniel Han
6a01c5559a
Studio: browser use follow-ups (safetensors nudge, date fields, disabled dropdowns, moved pages) (#13429)
* Studio: one post-tool nudge per browser turn on the safetensors loop

* Studio: let the browser agent fill date fields and refuse disabled dropdowns

* Studio: withhold a snapshot whose page moved while it was read

* Studio: pass the date-format error to the model; keep a withheld read's links out of the gate

* Studio: clear the stall repeat guard after each safetensors browser turn

* Studio: keep the safetensors browser nudge after retrieval was injected

* Studio: check a date value before touching the field

* Studio: gate Enter by the button it presses and secret dropdowns like their fields

* Studio: load the browser use toggle on use and re-measure the startup budget for #13372

* Studio: stub the browser tools in the token count parity harness

* Studio: find image submit buttons, honour disabled fieldsets, key secret dropdowns on their tag

* Studio: drop two comments that restate the code
2026-10-12 08:56:21 -07:00
Daniel Han
04f362f3b5
Keep uv, its cache and the uv-managed Python inside a custom UNSLOTH_STUDIO_HOME (#13373)
* Keep uv, its cache and the uv-managed Python inside a custom UNSLOTH_STUDIO_HOME

* Prefer the home's uv, keep its Python store at runtime and on update, and only isolate a usable cache

* Give the home's uv its own directory, keep it first on PATH, pass the Python store to engine installs

* Tighten comments

* Clean-machine Mach-O scan: require uv to be classified wherever it lives

* Read the private uv directory safely when Install-UvFromRelease runs under strict mode

* Opt in at runtime only where the custom-home installer created <home>/python and <home>/uv

* Claim the custom root before creating python/ or uv/ in it; replace a stale private uv

* Update: let setup version-check and repair the home's uv in place instead of putting it first on PATH

* install.ps1: let the stale flag reach Install-UvFromRelease past the inner version guard

* Leave main's files as main has them
2026-10-12 08:50:31 -07:00
Daniel Han
071e87e7d7
Studio: keep Spark-TTS LoRA checkpoints exportable (#13424)
* Studio: keep Spark-TTS LoRA checkpoints exportable

Restoring the Hub identity after a pinned-snapshot load rewrote
<snapshot>/LLM to the bare repo id, dropping the LLM subfolder, so
adapter_config.json named a repo root with no config.json and every
export (and inference load) of a Spark-TTS LoRA failed with
'Unrecognized model ... Should have a model_type key'.

Only rewrite the snapshot root itself, and load checkpoints saved before
this fix through a temporary copy whose adapter base points at LLM/.

* Tighten the Spark-TTS adapter loader docstring

* Move the Spark-TTS adapter loader to a torch-free module

So its tests import without torch and run on every CI runner.

* [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>
2026-10-12 08:47:28 -07:00
oobabooga
dbe40b1ad6 Revert "Studio: regenerate a chat's title from its latest turns (#13197)" 2026-10-12 12:10:35 -03:00
Lee Jackson
9b8338cb10
fix(studio): show queued downloads consistently across media routes (#13219)
* fix(studio): show pending download stages across media routes

* test(studio): cover empty panel after staged downloads

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* ci(studio): run the staged download rows Playwright driver

The new driver was not named by any workflow, so the Playwright CI
coverage guard failed and the regression never ran. Run it in the
extra shard next to the other image download drivers.

* ci(studio): upload the staged download rows driver artifacts

Keep its screenshots and facts.json with the other image driver logs so a
failure in that step can be debugged.

* fix(studio): clear queued media stages when the web session resets

Signing out in another tab clears this tab's download jobs, but the Images,
Video and Audio pages stay mounted and kept their staged queues published,
so the panel listed queued downloads that no longer existed. Clear the
staged queues in the same reset.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Etherl <61019402+Etherll@users.noreply.github.com>
2026-10-12 17:59:06 +03:00
Daniel Han
496a816194
Tests: catch up with four product changes that landed red on main (#13445)
test_return_logits_survives_for_training lifts for_training into a stub namespace that predates #13374's switch_training_mode, so the missing name came back as None. Stub it and the three-argument resolve, and make that namespace raise NameError on any unstubbed name.
test_windows_torch_flavor_invariant: count #13396's causal-conv1d + mamba-ssm slot in the torch totals and its call in step 13.
test_token_count_prompt_parity: stub #13372's browser agent store with its defaults; the count and the request both call browserAgentToolsForTurn.
test_diffusion_mlx_engine_router (from #13379): read install_python_stack.py as UTF-8 for test_source_read_encoding.
2026-10-12 07:56:28 -07:00
Daniel Han
1a2258b5e2
Studio: split DiffusionGemma across GPUs when one GPU cannot hold it (#13291)
* Studio: split DiffusionGemma across GPUs when one GPU cannot hold it

* Budget the diffusion training guard on the runner's own GPU fit; keep CUDA order for unpinned fits

* Size the diffusion training guard from the GGUF weights the launcher reads; keep a moved pick on dedup

* Only split DiffusionGemma across GPUs with a visual server that supports it

* Budget the runner overhead per device in the DiffusionGemma GPU split, matching the layer fit
2026-10-12 07:54:26 -07:00
Daniel Han
cfcf587af9
Studio: list each on-disk quant of a multi-quant GGUF in GET /v1/models (#13422)
* Studio: list each on-disk quant of a multi-quant GGUF in GET /v1/models

* Read quant pins from the published index, building it only when it never was

* Tests: accept quant_pins in the System One catalog double, compare pin rows without created

* Tighten comments in the quant pin listing

* [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>
2026-10-12 07:50:44 -07:00
boobcactus
f96409d08d
Unsloth Studio (AMD/Windows): import PyTorch before Triton to avoid WinError 126 (#10932)
* studio: import torch before triton on TheRock ROCm Windows builds

On TheRock ROCm torch wheels, importing triton first loads the driver's
System32 amdhip64_7.dll, after which torch_hip.dll fails to load with
WinError 126 (triton-windows#35). gate_torch_compile_on_windows already
imports triton at worker startup, so on those builds it must import torch
first. Detection is wheel metadata only (the +rocm local version segment),
so no torch import happens on CUDA or CPU builds, and a failed preload is
best-effort: triton is still asked the toolchain question either way.

* studio: log swallowed preload failures and stub the ROCm check in gate tests

Review follow-up: the file's fail-open idiom always logs at debug, so the
two new exception swallows now do too, and the module docstring names the
ROCm ordering responsibility. The pre-existing gate tests stub
_torch_is_rocm_build off so the suite never cold-imports the test host's
real torch.

* comment cleanup

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: detect AMD's untagged Windows ROCm torch for the triton preload

The +rocm version-tag check misses AMD's Windows torch 2.8 build
(2.8.0a0+gitfc14c65), so on that wheel the gate still imported Triton
first and reproduced the WinError 126. Reuse the import-free HIP-field
fallback from core._torchao_stub, which already documents that wheel
shape.

* Stub the compile probe in the msvc_env fallback tests so a real clang-cl on Windows CI cannot answer first

* Stub the compiler cache clear in the TinyCC tests so the Apple Silicon triton stub is never reached

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
2026-10-12 07:49:23 -07:00
Daniel Han
f2e1e10789
Studio: retry from the Hub when a cached snapshot has no tokenizer files (#13412)
* Studio: retry from the Hub when a cached snapshot has no tokenizer files

Transformers 5 loads a snapshot without vocab files as a one-token tokenizer
instead of raising, so the incomplete-cache Hub retry never ran and training
went on to NaN loss and reported success. Treat an empty tokenizer from a
pinned snapshot as a missing cache artifact, and name the cause when the
pinned Hub revision has no tokenizer either.

Fixes https://github.com/unslothai/unsloth/issues/6958

* Detect a vocab-less tokenizer by encoding, not by its length

A snapshot that kept tokenizer_config.json but lost the vocab file still
lists its special tokens, so a length check let it through.
2026-10-12 07:48:14 -07:00
Daniel Han
fb35485feb
Studio: keep emoji split across tokens in MLX replies (#13416)
Hold back a trailing U+FFFD in the plain MLX decode path and the batched row stream until the next token resolves it, so a character whose bytes span tokens is no longer streamed as a replacement character and lost. Fixes #8594.
2026-10-12 07:46:54 -07:00
Daniel Han
e6d9ff5a5d
Studio: refuse a text-encoder GGUF picked as the image or video model (#13423)
* Studio: refuse a text-encoder GGUF picked as the image or video model

* [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>
2026-10-12 07:29:22 -07:00
Eyera
1d3a81a5b5
fix(studio): preserve chat search position on close (#13437)
Restore focus without scrolling in both find-in-page cleanup paths. Cover Escape and close-button behavior across chat and outer scrollers in the browser regression suite.
2026-10-12 07:26:02 -07:00
Daniel Han
10dc580cc7
Studio: image-to-video for Wan2.2-TI2V-5B and LTX-2 / LTX-2.3 (#13378) 2026-10-12 07:24:49 -07:00
Michael Han
6126f3ec68
Studio: linked folders honour .gitignore, folder uploads in the browser, queued drops and retry for failed sources (#13224)
* Studio: move chat with files into a panel above the composer

Chat with files now has its own card stacked above the composer, with a
header (title, Project / This chat scope, Add files, paging arrows, close)
over a strip of the chat's documents drawn as composer attachment cards.

- Files reach the index only through the panel: its Add files button and
  drops on the panel. Composer attachments stay with the message.
- The panel picker reuses the composer's file chooser via openFilePicker.
- On a new chat the greeting, panel and composer center as one block.
- Light mode draws a border, dark mode keeps the composer shadow.
- Knowledge base chats and inherited project sources use the same panel.

* Studio: more bottom padding on the chat with files panel

* Studio: keep panel drops while busy and open previews on the project page

- A drop on the chat with files panel during an upload is refused with a toast instead of falling through to the message attachments.
- The project landing composer mounts the document preview, so its file cards open.

* Studio: read Office, OpenDocument, e-book, email, RTF and web page files in chat with files (#13081)

* Studio: read Office, OpenDocument, e-book, email and RTF files in chat with files

Adds parsers for .doc .xls .xlsx .xlsm .ppt .pptx .msg .eml .rtf .odt .ods .odp .epub using only the standard library, and lets the chat with files panel pick and drop them along with .csv .tsv .json .xml.

* Studio: address review on Office, ODF, e-book, email and RTF parsing

- RTF: ignore negative \bin lengths, and decode literal 8-bit text in the document code page.
- ODF: skip tracked deletions, keep bookmark references, read nested tables once, and cap text added by repeated cells and rows.
- EPUB: resolve URI-escaped manifest paths.
- OOXML: read Strict namespaces.
- PPTX: drop only the notes slide number placeholder, not numeric notes.
- XLS: format date cells from FORMAT, XF and DATEMODE records.
- DOC: include footnotes, endnotes and text boxes.
- MSG: decode String8 properties with the message code page, and fall back to the compressed RTF body.
- Desktop: route dropped .doc .xls .xlsx .xlsm .ppt .pptx .msg files to chat with files.

* Studio: harden RTF, compressed RTF, ODS and Excel date parsing

- RTF: pair UTF-16 surrogates from \uN so astral characters survive, and skip text marked \deleted.
- MSG: stop decoding compressed RTF at its declared size and refuse oversized bodies.
- ODS: read typed cell values written without a text paragraph.
- XLS and XLSX: treat the CJK and Thai built-in date formats as dates.

* Studio: read .ppt slides from the latest save, with text boxes and notes

Incremental saves keep old record versions in the stream, so slides from earlier saves were indexed too. The reader now follows the Current User edit chain and its persist directories to the live document, reads each slide's shapes in order (placeholders through OutlineTextRefAtom, plus text boxes), and appends speaker notes matched by slide id. A broken edit link continues with the next older edit; files without an edit chain keep the old scan.

* Studio: format Excel times and errors, and read RTF font charsets

- XLSX and XLS: time-of-day and [h] duration formats keep their own text, and serials before March 1900 account for Excel's 29 February 1900, as openpyxl does.
- XLS: error cells and cached formula errors read as #DIV/0!, #N/A and so on, and long formula strings continue into CONTINUE records.
- RTF: bytes decode with the active font's charset or code page, and groups nested past 1000 levels are refused.
- MSG: Exchange senders get their SMTP address.
- EML: an empty plain part falls back to the HTML part.

* Read charts, number formats and package rels; bound XML parsing

- Find the main OOXML part through _rels/.rels, and sharedStrings and
  styles through the workbook rels.
- Stream sheet rows and shared strings with per-element, per-row and
  depth caps so large or crafted parts cannot exhaust memory.
- Show Excel numbers with their display format (percent, grouping,
  scientific, literals, currency, sections).
- Add pptx chart titles and cached series data.
- Keep internal empty ODF cells, drop only trailing ones; stream .ods rows.
- Prefer the Unicode branch of RTF \upr groups.
- Join every inline body part of mixed emails.

* Read Office macro, template and slideshow files, OpenDocument templates, MHTML and XHTML in chat with files

* Decode Excel 5.0/95 byte strings with the workbook code page; bound the compound file FAT to the file size

* Refuse EPUBs with DRM encrypted chapters; leave oversized number formats as stored

* Skip the upload and index test where sqlite-vec cannot load

* Trim comments

* Read SmartArt labels and string HTML bodies; charge reused shared strings to the repeat budget

* Read scatter chart caches; bound .doc text to the stream size

* Keep each scatter series' X values; fall back from a blank .msg plain body

* Studio: name the compound file stream reader read, not open

* Studio: let projects and knowledge bases take Office, OpenDocument, e-book and email files

* Studio: chat with files panel takes desktop drops, groups linked folders and shows a file count

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Studio: draw the light chat with files panel edge as a crisp hairline

* Studio: file thumbnails and the attachment viewer for chat with files cards, info tooltip, flat panel edge

* Studio: linked folders honour .gitignore, skip build output and data dumps, keep syncing past the file cap, and report every failed file

* Studio: open large sources on click and read EPUB package paths as URLs

* Studio: linked folders skip empty files and report renames

* Studio: folder uploads in the browser, queued drops, retry for failed sources, grouped linked folders and quieter folder syncs

* Studio: cap legacy spreadsheet cells and cache compound file directory lookups

* Studio: file count pill, title tooltip, and the viewer size limit for source cards

* Studio: key the upload queue by chat, cap folders after filtering, keep Retry when a delete fails, and skip malformed gitignore rules

* Keep dropped folders with their destination, stop walks at the cap, fail closed on unreadable .gitignore

* Match .gitignore globs without exponential backtracking

* Cap card thumbnails by bytes and release them off screen

* Open the files panel tooltip only on resting over its title

* Decode files panel text previews like composer attachments

* Drop a UTF-8 BOM before reading .gitignore rules

* Show the files panel count always, with the description on its hover, and strengthen the light edge

* Decode Mac and PC RTF character sets, and keep every level of chart categories

* Stream OpenDocument spreadsheet templates like spreadsheets

* Keep a retryable failed row when a refresh sees the failure first

* Type the lifecycle harness's refresh

* Skip pnpm-lock.yml in browser folder uploads as the scan does

* Trim comments

* Trim comments

* Keep the note in the folder-name catch

* Lighten the chat with files border in light mode

* Lighten the chat with files border to 10% in light mode

* Move the light-mode border change to its own PR

* Expect a failed upload or index to stay as a failed row in the upload browser cases

* Trim comments in the linked-folder and source upload changes

* Drop restating comments in the source upload changes

* Re-measure the Studio startup budget for the project sources follow-ups

* [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>
Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
2026-10-12 07:18:00 -07:00
Etherl
fe1709fe62
Revert "Unsloth Studio / Desktop: colour a companion download's bar by model, text encoder and VAE (#13112)" (#13431)
This reverts commit eb84cce811.
2026-10-12 07:09:47 -07:00
Daniel Han
465aef844c
MLX diffusion backend: decode the sysctl chip name as UTF-8 (#13430)
#13379 added a subprocess.run(text=True) with no encoding in _chip_name, which test_text_io_encoding flags on main (it would fall back to the locale codec under a C/POSIX locale). Pass encoding='utf-8', errors='replace' like the other inference subprocess calls, and apply the formatter hook to the file.
2026-10-12 07:08:15 -07:00
Daniel Han
2a81fc4f8c
Studio: pin sd.cpp to the mirror build with Windows CUDA, sharing llama.cpp's CUDA runtime (#13175)
---------

Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-10-12 10:43:51 -03:00
Michael Han
7703901eac
Studio: let models use the browser panel (#13372)
* Studio: let models use the browser panel

Browser tools (navigate, snapshot, find, click, type, select, scroll, press key, screenshot) that any tool loop can call: GGUF, safetensors/MLX and external API providers. The chat tab runs each action in the agent's tab of the browser panel and posts the result back; the panel asks the user before sign-ins, POST forms, purchases and new sites, and never types into password or payment fields.

* Trim browser agent comments

* Studio: browser use settings, site permissions and history access

Settings > Browser gains a Browser use section: turn browser use on or off, choose whether models may read the browsing history (always ask, always allow, never; a new browser_history tool), and Agent permissions with a default and per-site rules for browsing, actions (sign-ins, form posts, purchases) and downloads. Password and payment fields stay off-limits whatever the rules say.

* Studio: agent permissions wrap instead of cutting labels

* Studio: tighten browser agent gates from review

Gate Enter/Space on buttons and cross-site form actions, re-check the landing site after redirects, honour submitter formaction/formmethod, redact card and secret field values, skip hidden elements in find, keep refs unique across pages, fence the page title, stop the running action on Stop, size the tab's deadline from the backend wait and retry result posts.

* Studio: use the square dashed pointer icon for annotate

* Studio: close remaining browser agent gate gaps

Recheck the page's site before every read or action, gate checkbox selects and Enter/Space on submit buttons, submit on Enter only from text fields, find password fields joined with form=, build control names from visible text, keep page words out of errors, mask secret fields in screenshots, expire queued actions from receipt, give each page a non-overlapping ref range, and apply the initiating site's download rule.

* Studio: gate direct links, localized actions and protected-field deletes

Ask before following a link whose href is read at click time, submit Enter through the button the gate described, read headings from visible text, keep site-name approval off shared hosts and partial host matches, hold Backspace/Delete to the typing rules, and recognise consequential labels in Studio's UI languages.

* Studio: fence page script errors, stop WebKit double submits, tighten site matching

- A page script's error text reached the model outside the untrusted fence. The panel now
  sends it as pageError and the server fences it.
- WebKit acts on a synthetic Enter keypress (implicit submit, button click), so Enter in a
  field or on a button ran twice there. press() now skips its own action when the key
  already submitted or clicked.
- A trailing dot (evil.com.) slipped past blocked site rules and the website policy on
  link, redirect and form paths.
- Naming a site ("search wikipedia") no longer approves a lookalike on another TLD
  (wikipedia.xyz) without asking.
- example.com vs *.example.com on a subdomain no longer depends on save order.

* Studio: keep page errors out and gate dropdown changes and delete keys

Pass only the page agent's own fixed error messages to the model, judge a dropdown change by the option chosen, and ask before Backspace or Delete outside a text field.

* Studio: scope browser agent approvals and download rules, drop tiny text

- Following a page's link no longer approves its whole site: the model can open that
  exact link, but an address it makes up there still asks. Approvals are per chat.
- The agent's Downloads rule applies to its tab only while a chat run uses it, so the
  user's own downloads there go back to their own settings afterwards.
- Browser tools are offered only to requests that take UI frames: an API caller that
  named one waited out the whole timeout for a tab that never got the action.
- Text under 2px (font-size:0 and similar) is left out of the page snapshot.

* Studio: keep agent errors on the allowlist path only

The page agent's error wording is now allowlisted in the panel, so page words can't reach an error. The separate pageError route fenced the agent's own guidance as untrusted text, so it goes.

* Studio: let the browser tools repeat a call

The tool loop refused a call identical to one that already succeeded, and two refusals took the tools away. A browser call acts on or reads the page as it is now, so a second scroll down or a snapshot after a click was refused, and the model was left announcing its next step with no tools to take it.

* Studio: keep identical browser calls in one batch

The GGUF and safetensors loops dropped a repeated identical call within one model turn without telling the model, so two scrolls or two Tab presses ran once.

* Studio: keep browser tasks going when a step is announced but not taken

- GGUF: the post-tool "you said you'd act, now act" nudge was one per request, so the
  second "Let me take a screenshot..." in a browser task ended the turn. With browser
  tools on it is one per tool turn, as the external loop already does.
- External providers: browser turns follow the chat's nudge setting instead of never
  nudging.
- An action that runs out of time says so, instead of reading as the user declining or
  the tab closing.
- If the browser code can't load (a stale chunk after an update), the tab answers at
  once instead of leaving the tool waiting for its whole timeout.

* Studio: close browser agent gaps from review

- The proxied frame keeps the agent object it installed instead of reading a window key a
  page script can set first.
- aria-labelledby names come from visible text only, and contenteditable fields with a
  secret name are reported as filled or empty, like inputs.
- A label is described as the control it clicks, so a label for a submit button is gated
  as a submit.
- Links a page follows itself (native views, Enter on a link) and history moves with a
  known destination are gated before they load.
- The website policy matches whole hostnames, as the server does (www.example.com no
  longer allows its siblings).
- A declined action is reported by kind and host, not the page's label for it.

* Studio: gate the dropdown option that is selected, and other review fixes

- A dropdown is gated on the label of the option the page will select, not the value the
  model sent, which can differ from it.
- Buttons are classified by their normalized type, so an invalid type attribute counts as
  submit, as in HTML.
- Screenshots mask secret contenteditable fields too.
- A blob: download follows the Downloads rule of the site that made it.

* Studio: use the current cursor-in-window icon for browser use

* Studio: per-chat browser grants, nested control refs, download rule timing

- Browser grants reset on any change of chat; actions resolve their chat per call, so a
  first turn is re-keyed to its own id instead of carrying "__default" grants onward.
- A ref inside a button or label is classified as the control its click reaches.
- A proxied download reads the agent's Downloads rule when the load starts, so a slow file
  still gets it after the run ends.
- Browser use is disabled in the composer where the panel can't show (mobile, compare).

* Studio: browser agent editing and screenshot mask fixes from review

- The screenshot mask covers secret fields in open shadow roots and hides frames, whose
  fields the page script can't read.
- Backspace and Delete edit at the caret or selection, and Delete is handled at all.
- Unchecking a selected radio reports that it can't, instead of a false success.
- A covered link is refused like any covered element, before it is followed.
- Disabled and read-only fields are not typed into.

* Studio: one chat per agent tab, native history gating, field keys

- Only one chat drives the agent tab at a time; another chat's browser call is told to wait.
- Back, Forward and Reload are checked before they load: from the tab's history, the current
  page for a reload, or, on native views, where the agent's own moves in this chat landed.
  A native move to an unknown page asks first.
- Caret keys move the caret in a field, Backspace and Delete work in contenteditable, and a
  key that does nothing in a field says so instead of reporting success.

* Studio: whole-word site mentions, bounded result posts, Stop for the whole run

- A site the user named matches whole hostnames and words only, so a name inside another
  hostname (collector.example.com.evil.test) no longer approves it.
- Posting an action's result times out, so a hung request can't hold later actions.
- A native Back, Forward or Reload marks the tab loading at dispatch, so the result is the
  page it moved to.
- The agent's Downloads rule outlasts the run briefly, for downloads reported after it ends.
- The agent bar and its Stop stay up for the whole run, between actions too.

* Studio: put Browser use under Chat with files and MCP under More

Browser use now follows Chat with files in the composer + menu (or leads that section when Chat with files is unpinned). MCP moves into More by default; a one-time migration of the saved menu pins applies this to existing installs too, and MCP can be pinned back from More.

* Studio: keep browser-use hook order stable when a tool-less model loads

* Studio: withhold browser pages that land on a private or local host

* Studio: offer browser tools on the managed engine path only with UI frames

* Studio: a named brand approves its own site, not its subdomains

* Studio: hide password values behind a role override and sub-2px text from every page reader

* Studio: check where a scroll lands before the model reads it

* Studio: the external nudge wiring test allows the browser-use exception

* Studio: drop browser agent comments that restate the code

* Studio: submit a new-tab form into the agent's own tab

* Studio: keep new-tab forms and links in the agent's tab past the shell's deferred routing

* Studio: drop a screenshot whose page moved during the capture

---------

Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
2026-10-12 06:42:40 -07:00
Daniel Han
9f4a868d43
Studio setup: install causal-conv1d and mamba-ssm prebuilt wheels next to flash-attn (#13396)
* Studio setup: install causal-conv1d and mamba-ssm prebuilt wheels next to flash-attn

Setup already installed flash-attn from prebuilt-wheels-cu13 but left
causal-conv1d and mamba-ssm to the first Mamba / hybrid model load. Install
both at setup through the same wheel-only install_kernel the CLI and SSM
runtime use: never a source build, never a torch change, skipped where no
wheel is published, GPU-gated for mamba-ssm, and resynced wherever
flash-attn is after a torch repair. UNSLOTH_STUDIO_SKIP_SSM_KERNELS_INSTALL=1
opts out.

* SSM kernel setup step: local check first, bounded probes, fresh runner, no-GPU install, stale removal

Skip the wheel lookup when the resident kernel already loads, so an offline
re-run does not wait on two HEAD requests. Each call gets its own runner,
which bounds the import probes and resets install_kernel's per-runner GPU
and Triton cache after a torch repair. Setup installs mamba-ssm where no GPU
is visible yet (an image build); a visible pre-Ampere GPU still skips it. A
resident build that no longer loads and has nothing replacing it is removed,
as flash-attn's resync does.

* SSM kernel setup step: honour UV_OFFLINE

No wheel lookup and no install when uv is told to stay offline: the pip
fallback in install_wheel does not read UV_OFFLINE. A kernel that already
loads is still reported.

* SSM kernel setup step: offline still removes a stale build; a probe that cannot launch is inconclusive

* SSM kernel setup step: bound the torch probe on every platform and skip on a launch failure

* SSM kernel setup step: an uninstall that cannot launch warns instead of aborting

* Trim comments

* SSM kernel setup step: redact captured install output before printing it

install_kernel only replaces the wheel URL; uv / pip failure text or an
exception can name another index carrying credentials.

* Tests: pin the no-GPU mamba-ssm case to Linux so Windows' Triton gate does not answer first
2026-10-12 06:23:20 -07:00
Eyera
7be2c951e2
fix(studio): show original chat model in the header (#13398)
* fix(studio): show original chat model in the header

* fix(studio): retain chat model history across navigation

* fix(studio): keep original model control clickable in narrow headers

Reserve space for the original-model icon and let its label spacing truncate with the label. Tighten mobile picker spacing when the control is present and use the compact context icon in single-chat headers while retaining the full token count in the tooltip.

Validated against upstream with Chromium and Firefox, including production CSS and blocked external network access. All 11,745 frontend tests, typecheck, production build, and bundle budget checks pass.
2026-10-12 06:19:19 -07:00
Daniel Han
d6cfacfbfb
Keep syntax colour on settled thinking fences in tilde, four-backtick, indented and stopped forms (#13415) 2026-10-12 06:17:23 -07:00
Daniel Han
4e49365d60
Studio: keep Data Recipe runs whose structured fields change type between rows (#13407)
* Studio: keep Data Recipe runs whose structured fields change type between rows

* Studio: give every Data Recipe batch file one schema at the end of a run

* Studio: keep Arrow columns whose type already matches across batch files

* Studio: reconcile batch schemas one column at a time and promote with Arrow first

* Studio: cast promotable batch columns per file instead of holding them

* Studio: store a path mixing scalar kinds as text instead of re-inferring it
2026-10-12 06:16:52 -07:00
Michael Han
3075b73d17
Studio: browser page zoom on desktop: zoom keys and clipping (#13401)
* Studio: zoom keys zoom the browser page after clicking into it

On macOS a click into a native page view moves the keys there, but the app's activeElement stays where it was (often the composer), so the View menu's Cmd+= / Cmd+- zoomed the whole app. The menu now asks the zoom scopes first: the browser's claims the keys while its page view holds them (the app document has no focus and a page is shown).

* Studio: keep native browser pages whole when the app is zoomed

The panel cuts a hole in its backgrounds for the native page with a fixed-attachment background. Under the desktop app's interface zoom (the webview's page zoom) WebKit scaled that rect by the zoom once more than the layout it came from, so a zoomed-out app covered the right and bottom of the page. The rect is now divided by the applied zoom and placed again when a zoom lands.
2026-10-12 05:49:58 -07:00
Daniel Han
592be14608
Studio: Wan2.1 T2V 1.3B and 14B video families (#13376) 2026-10-12 05:29:39 -07:00
Rudra Bhaskar
135dca33cd
Studio: document the intentional torch and torchcodec floor gap (#10635)
* Studio: document the intentional torch and torchcodec floor gap

* Test both shell floors and patch-version constraints

* Tighten torch 2.4 torchcodec notes and check every install.sh floor

---------

Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
2026-10-12 05:17:16 -07:00
Daniel Han
10bddef22e
Studio: opt-in MLX image engine for Apple Silicon (Z-Image) (#13379) 2026-10-12 04:48:45 -07:00
Daniel Han
1f1b9e78d8
Studio: refresh the context meter when a tool loop hands off to a tool (#13356)
* Studio: refresh the context meter when a tool loop hands off to a tool

* Studio: strip a provider's forged _contextUsage key like the other control keys

* Studio: match the queue contract test to the shared context meter write

* Studio: send the context meter update once the tool is running, so it never delays the tool
2026-10-12 04:46:55 -07:00
Daniel Han
27626031e6
Studio: judge hosted pre-quant checkpoints before downloading them, and make the int8 fingerprint torchao-version-neutral (#13339) 2026-10-12 04:15:13 -07:00
Daniel Han
d3e21f3ee2
Studio MPS: opt-in fp16 compute with fixed headroom for Z-Image (#13375) 2026-10-12 04:15:09 -07:00
Daniel Han
cfe6d9344d
Studio: refuse LTX-2.5 instead of loading it as LTX-2 (#13370) 2026-10-12 04:15:04 -07:00
Daniel Han
0464be9dad
Studio: sd.cpp on Macs, CPU backend on a paravirtual GPU and no direct conv on Metal (#13369) 2026-10-12 04:15:00 -07:00
Daniel Han
072c44cedf
Sandbox delete test: only the chat's detached tree counts as the delete (#13403)
test_deleting_a_big_sandbox_does_not_hold_up_other_chats patches shutil.rmtree for the whole process and took the first call as the start of the delete. Any other rmtree (the legacy migration remove_session_sandbox starts, a background cleanup left by an earlier test in the xdist worker) set it before the rename, and the test failed with 'the name is still there' (seen on #13390). Pass every other path straight through and gate on the detached '.deleting-' tree, which also proves the rename came first.
2026-10-12 04:10:48 -07:00
Eyera
e36527ddcb
Perf/scroll performance (#13255)
* Chat: keep long replies scrolling smoothly while they stream

Long reasoning transcripts made the whole window sluggish: every token
publish re-rendered the message tree on the sync lane, two mutation
observers forced layout per publish, the open code fence was re-lexed and
rebuilt three times at completion, and on Windows the custom scrollbar
dragged the thread on the main thread with a full relayout per hover.

- Render streaming text on a cost-aware cadence instead of per publish,
  and keep the open fence to a line window while it streams.
- Read nothing from layout inside the autoscroll mutation callback; pin
  instantly at run start so the follow never detaches.
- Drive the message list through a portal so the library's resize
  observer stops measuring the viewport, and skip content selectors
  while a message is running.
- Own the strings sliced out of the transcript so V8 drops the parents.
- Arm the zoom wheel listener only while Ctrl/Meta is held.
- Give the thread Chromium's standard thin scrollbar on Windows and
  scope the hover reset to the lists that need it.
- Mount the response details sheet only while it is open, skip
  checkpoint saves during a durable run, and let find-in-page observe
  the document only while it is open.

* Chat: keep very long threads responsive with every thinking box open

Long replies with 100K+ reasoning traces and 900-line fences made the whole
UI sluggish once their boxes were expanded: 29.7K DOM elements at rest,
half-second reopens, 300-450 ms layouts on every width, font or theme change,
and blank bands while scrolling the traces.

Fences: FenceMode "window" is the ship default (240-line cap). Off-screen lines
of a long fence render as one text node, the body carries its height in
--unsloth-fence-height so the engine can skip it under content-visibility: auto
(code-block containment flag, same gate and ablation switches as the math one),
line windows measure from the body's computed line-height instead of descendant
geometry and skip frames while the scroller is flying. Fences past the
highlight limit are tokenized in the worker and seeded into the fence cache.

Reasoning transcript: rows in flow instead of a virtualizer. A character and
pixel budgeted progressive mount (4K chars first, 24K chars per frame shared
across open transcripts, quarter budget during the first 300 ms) with a
trailing spacer of estimates, rows settle after two frames and only then
become skippable (reasoning-row containment flag), code fragments are grouped
per fence under one one-way IntersectionObserver with worker highlighting whose
replies keep identical line arrays. Reading position: captured per scrolled
frame with one reused Range (a Range per text node per frame left thousands
attached until GC and made every DOM removal walk them), ignored in frames the
engine's own scroll anchoring shifted before the width handler ran, restored
across reopen, threshold hand-over and width changes.

Store selectors: per-message-parts derivations and compaction notice owners are
memoized on the array identity, the copy button reads the store at click, the
thread list filters by message counts.

Measured on the user's 11-message thread (two 130K traces, two 900-line
fences): DOM 29.7K -> 4.4K elements; reopen 528-758 -> ~270-500 ms; width
change 330 -> ~70 ms, font scale 447 -> ~150, theme ~310 -> ~150; blank bands
while scrolling expanded traces 333 -> 85 px at 300 px ticks and 778 -> 268 px
at 600 px; Windows Chrome wheel p50 16.6 -> ~12 ms with all boxes open and no
checkerboarded frames collapsed. Maintainers' transcript checks pass 15/15 on
chromium; the code-block flicker contract is clean on chromium and firefox.

* Chat: fill long thinking traces without moving the reader, on every engine

With every thinking box open on a long thread (two 130K-character traces,
two 900-line fences), opening, jumping, resizing and streaming still cost
long frames, and on Firefox and WebKit the reading position slipped or jumped.

Reasoning transcript
- Widening is commit-bounded: one shared frame budget (24K characters and 64
  rows across every open transcript), a starved request is re-issued, and the
  limit always lands on a chunk boundary of a fixed grid, so chunk keys never
  change and no row is ever remounted.
- Jumping into an unmounted stretch mounts an island of rows under the viewport
  at once. Islands only grow and keep their identity; every unmounted stretch
  is the spacer of the island below it or of the tail, and a closed stretch
  keeps a zero-height spacer in place. Commits therefore only insert or restyle
  nodes: WebKit lays a size container out mid-style-resolution, and removing a
  tall spacer threw a reader parked at the bottom 120K px up (HEAD jumped at
  cover).
- The limit and the islands carry the document they were computed for, so a
  widening, print or re-issue still queued for a replaced document can never
  bring its prefix back.
- Chunks of rows are the content-visibility units. They are their own
  formatting context while containment is on, so settling no longer drops a
  code group's collapsed margin for a frame and makes a following reader
  detach. A chunk settles only after a whole frame has laid it out at the
  current width, counted per row; one whose rows render again before it
  settles waits a fresh frame, and a transcript shown again after rows settled
  while it had no box is laid out again.
- The reading anchor is held relative to the transcript root: corrections are
  whole pixels and the sub-pixel remainder carries into the next anchor, so
  Firefox and WebKit no longer creep 4-14 px over a fill. A trace wholly above
  the viewport undoes its own growth, measured across each commit, which
  WebKit otherwise leaves a reader in a later trace to slide under.
- Code groups position their own copy and download actions. A print mounts
  every open trace in one synchronous commit and latches the fences it
  brought. The parsed index is cached per reasoning round.

Highlighting
- The highlight worker answers a ready handshake; a slow boot is "stalled",
  not "unavailable", so speculation resumes when it recovers, while a fence's
  whole-body request keeps waiting on the late worker. A fence over 20K
  characters is never tokenized on the main thread outside a print: without a
  worker it stays plain rather than freezing 0.3-0.5 s per fence. A request
  that never settles fails after 15 s, so it cannot hold a speculation slot.
- Settled fences are pre-tokenized at idle (budgeted, up to three in flight,
  paused while a reply streams or the worker is stalled) and seeded exact-hit
  only. Exact requests fork a shared prefix entry, so sibling fences that
  extend one another are tokenized once each and none keeps a plain tail.
- Common grammars are warmed at idle after the first quiet moment. A selection
  inside a windowed fence pins only the lines holding its boundaries, and a
  drag freezes the window until the button is released (WebKit sends neither
  pointerup nor pointercancel after a text drag, so dragend, drop and a
  buttonless move release it too).

Thread
- A scroll-up that arrives with a shorter page is content leaving, not the
  reader leaving the bottom, unless a scroll-up key was just pressed.
- The message rhythm rule no longer uses a universal sibling combinator, which
  made Blink restyle every mounted row on each insertion.
- Thread reads are shared while in flight, the documents bar follows the row
  it announces instead of re-reading on every project change, and the split
  handle releases on blur or a buttonless move.

Measured on the user's thread with every box open, against HEAD: fast wheel
60.0-60.2 vs 56.0-56.5 fps, 0 vs 8-10 frames over 33 ms, 1-18 vs 402-468 ms
dropped, script 560-563 vs 1,809-1,864 ms, style 26-27 vs 310-352 ms;
Windows Chrome expanded wheel 57.2 fps, 23 ms worst, 13% busy vs 55.8 fps,
105 ms, 37%; resize keeps the passage with zero drift. WebKit: a reader at
the bottom or mid-trace is no longer thrown (HEAD 70-99K px at cover), and a
reader in a later trace stays within 2 px while an earlier one fills (HEAD
jumps 23K px); a jump into the later trace while both still fill can take one
326 px step. Fast streams keep following in Chromium and Firefox (lost 4/4
before). Maintainers' transcript checks pass 4/4 in Chromium on dev and 2/2
on a production build.

* Chat: attach files dropped on a message again, keep the split's contract

The thread's rows render through a portal from a hidden host, and React
routes their drag events to that host instead of ThreadPrimitive.Root, which
owns the page-wide drop handlers. In a browser a file dropped on a message
was therefore never attached: dragover was not cancelled and Chromium opened
the file in a new tab. The host now carries the same four handlers. Each
native event still reaches exactly one of the two, so the drag depth stays
balanced and the composer's drop affordance shows and hides as it did before
the portal. Unsloth Desktop was unaffected, since its native drop path skips
these handlers.

The split handle's width collapse and its disabled hit test read
`!artifactLayoutActive || browserFullView` directly again instead of a
hoisted constant, which is the expression the deep research frontend
contract pins (tests/studio/test_deep_research_frontend_contract.py).

* Chat: fit upstream's Browser overlay and 12px code font

Upstream's Browser overlay (#12804) is an inline aria-modal dialog. This
branch had replaced the `html:has([aria-modal="true"])` rule that kept the
find bar under modals with document flags, and nothing sets one for an inline
modal, so on the first open of a session (compare mode, phone width or a
project view) the find bar stayed drawn over the overlay for 300-450 ms,
until the lazy browser panel loaded. The overlay now flags the document while
it is mounted, and a separate rule hides the find bar from the overlay's
first frame. The zoom popup keeps upstream's behaviour.

Windowed code blocks reserve their height before they first render, from
their line count times the line pitch. The pitch came from the body's
computed line-height, which upstream's 12px code font (#12805) moved off the
1/64 px grid Chromium and WebKit lay lines out on: at non-default UI sizes
or interface scales a 900-line block reserved 3-11 px too much and moved the
reader when it rendered. With a custom Code font size the body never
reported the code's line-height at all, so the reservation was off by
hundreds to thousands of px and visible lines could fall outside the line
window and lose their colours. The pitch is now measured from layout, once
per font setting and device pixel ratio, with a hidden 64-line probe built
like the block's own lines and placed beside the body, so a skipped body is
never laid out. On this thread's two 900-line blocks, at UI sizes 15-19 and
custom Code sizes 13 and 16, the reserved height now matches the rendered
one within 0.02 px in Chromium, Firefox and WebKit (before: up to 11 px, and
up to 6,600 px with a custom size).

* Chat: step the thread's width layout without a size container

WebKit restyles every element under a size container whenever the
container's inline size changes, so with the thread root as an
`@container` every pane drag, sidebar toggle and window resize restyled
the whole thread. On the long test thread (15K elements, every thinking
box open) that cost about 400 ms per step on WebKitGTK, and Unsloth
Desktop runs on WebKit on macOS and Linux.

The root is no longer a container. A ResizeObserver sets
data-thread-narrow (31.5rem and under), data-thread-tiny (18.75rem and
under) and data-thread-wide (36rem and over) on it, with rem read from the
root font size as a container query reads it, and the rules that queried
the container select on those attributes inside :where(), so their
specificity is unchanged. The thinking pill keeps its own container query
on the composer surface. A print is laid out at the page's width, which no
observer reports, so print takes the wide code header a page always got.

Dropping the container also stops a WebKit scrollTop clamp. Under a size
container WebKit lays the document out in the middle of style resolution,
so a commit that removed or re-keyed a tall element, such as streamdown
replacing a block of a streaming reply, clamped a following reader's
scrollTop and read as a scroll-up: a stream started from an empty thread
was never followed. Setting container-type: normal on the root alone
stopped it.

WebKitGTK 2.44 / 2.50 / 2.52 on the long thread: a pane resize step
405 / 383 / 450 ms -> 89 / 43 / 61 ms (p50), a drag frame about 400 ms ->
20 ms, the sidebar toggle's first frame 460 / 445 / 528 ms ->
186 / 117 / 160 ms, and moving the split with the arrow keys 2 fps ->
11 / 23 / 18 fps. In Chromium the forced layout per step halves
(8-14 ms -> 4-5 ms); the one frame in which the width crosses a step does
an extra layout, 66-108 ms against 32-35 ms.

* Chat: keep skipped blocks sized right across a desktop zoom

WebKit remembers a `contain-intrinsic-size: auto` size in the zoom it was
laid out at, and lays out contain-intrinsic-size lengths without the page
zoom (WebKit bug 308137). After an interface scale change in Unsloth
Desktop on macOS and Linux, every skipped reasoning chunk, windowed code
body and maths block above the reader kept a height from the old zoom,
and the thread jumped as they came back on screen.

While setZoom runs, and for 300 ms after, html carries
data-interface-zooming and those blocks render, so WebKit measures them at
the new zoom before they are skipped again. WebView2 rescales remembered
sizes itself, so Windows skips the hold, and a setZoom that never settles
releases it at the first-paint deadline. A code body's reserved height is
scaled by a measured contain-intrinsic-size factor, which the line-pitch
cache also keys on, since devicePixelRatio ignores page zoom on WebKitGTK
2.50 and Safari 26.3 and earlier.

On Windows the thread's scrollbar gutter now divides by the zoom: the
native thin scrollbar keeps its size when the page zooms, so the gutter
reserved for it grew and shrank with the interface scale.

Off macOS a trackpad pinch arrives as Ctrl+wheel with no keydown, so the
wheel listener that turns it into an interface zoom also arms while the
pointer is over a zoom scope.

WPE and WebKitGTK 2.52, zooming the test thread: display maths measured
wrong afterwards 119 of 120 -> 0 of 120, and the reader's passage moved
850-900 px -> 2-93 px. Reasoning and code at 80% and 90%: median drift
27-31 px -> under 1 px.

* Chat: keep code highlighting off WebKit's main thread

JavaScriptCore never compiles a regular expression with a lookbehind to
native code, and Shiki's grammars are full of them, so highlighting runs
5-25 times slower on WebKit than in V8. Unsloth Desktop on macOS and Linux
stalled for 0.5-1.6 s whenever a settled code block, or a block waiting
on a worker that could not answer, was tokenized on the main thread.

On engines without requestIdleCallback, which today means WebKit:
- a settled code block is tokenized by the highlight worker, unless the
  main thread's own cache already covers all but its last 512 characters,
  as it does for the block that just streamed. Whatever the cache already
  coloured stays on screen while the worker finishes, so a block never
  goes plain on the way;
- a reasoning code group falls back to the main thread only up to 2,000
  characters;
- grammar warm-ups, the fallback queue and scheduleIdleTask's fallback
  wait for 300 ms without input, within their deadlines, so none of them
  lands in the middle of a scroll.

On JavaScriptCore, told apart by its user agent so the worker makes the
same choice, regular expressions are built for an ES2018 target, which it
compiles several times faster than the default target's v-flag patterns.
V8 matches the emulated patterns about 7% slower, so Blink keeps the
default.

The worker client: a request for a block the main thread could tokenize
no longer waits out a late boot; a ready worker that sends nothing for
15 s while such a block waits is treated as late until it answers; and a
second evaluation of the worker's entry, which WebKit runs when a grammar
chunk imports it back, keeps the first message handler. The grammar
prewarm waits for 1 s without input. Reasoning line tokens keep a
coloured line over an uncoloured reply for the same text, and a reasoning
code fragment re-renders when any of its lines' tokens change, not only
its first line's.

WPE and WebKitGTK 2.52 under tauri:// with the app's CSP, streaming three
code blocks: the longest main-thread stall 497-708 ms -> 218-238 ms, and
every block ends coloured, where one or two used to stay plain. With the
worker missing, 60 s late or silent, every block up to 20K characters ends
coloured, with stalls of 353-406 ms against 1.0-1.6 s before.

* Chat: keep a streaming reply followed on WebKit past a long code block

A windowed code body is skipped with content-visibility: auto and sized
by contain-intrinsic-size: auto var(--unsloth-fence-height). It was marked
windowed on its first render, before the measurement that writes that
height, so an upgrade off screen was first laid out as an unsized skipped
body and collapsed. When the previous reply's 900-line block upgraded from
plain to highlighted about 1.8 s into a new stream, the thread shrank by
15,600 px for that layout. Desktop WebKit has no scroll anchoring, so the
following reader's scrollTop was clamped and stayed there, which read as
the reader scrolling up and stopped following for the rest of the reply.

The body is now marked windowed only once it has been measured, and its
height is compared with the element's own declaration rather than the
last value written, so a body that remounts is given its height again.

The reasoning transcript's hold, which keeps a detached reader's passage
still while rows above it grow, now learns whether the scroller acted on
its correction even when WebKit applied none of it, so the unapplied part
carries to the next commit instead of being lost.

Real app on WPE, with and without scroll anchoring, streaming after the
long thread: followed to the end in 6 of 6 runs, against 0 of 6 before.
The smoke harness follows 24 of 24 streams on WebKit, Chromium and
Firefox and 12 of 12 on WebKitGTK 2.50 and 2.52, and wheel, PageUp,
selection drag, find and scrollbar still detach exactly as before
(64 of 64, and 12 of 12 on WebKitGTK).

* Chat: keep the split drag and find-in-page out of the reader's way

Dragging the chat split set a `:root[data-chat-split-resizing] *` cursor
rule. It matches every element, so starting and ending a drag restyled
the whole document: 55-69 ms in Chromium and about 320 ms in WebKit on
the long test thread. The drag now places a transparent full-window
overlay at the drag-cursor layer for its duration, which carries the
cursor and keeps the pointer off the panes and their iframes; starting
or ending a drag costs 18-21 ms on both engines.

Find-in-page rebuilt its index on a timer even while the reader was
scrolling. A rebuild now waits until no wheel or touch movement has been
seen for its interval, and autoscroll's own scrolling does not count, so
a followed stream still gets its rebuilds.

* Chat: shrink the reasoning widening step where commits run long

A reasoning transcript mounts its rows a bounded step per frame. On
engines without requestIdleCallback the step now scales by how long the
last growing commit took, aiming at 16 ms, never below an eighth of the
full budget and at most doubling back per frame. Blink commits a full
step inside a frame and keeps the fixed budget.

WebKitGTK 2.52, expanding and scrolling the long test thread: frames over
33 ms 22 -> 11, over 50 ms 8 -> 3, p90 38 -> 27 ms, for a fill 13% longer
(3.9 s -> 4.5 s).

* Chat: keep the desktop titlebar's backdrop dim from restyling the window

The custom Windows/Linux titlebar dimmed itself under a modal through
`body:has(> [data-viewport-backdrop="true"][data-state="open"])
[data-slot="window-titlebar"]::after`. That made <body> a non-subject
:has() anchor, and checking its children marked #root as inside a
:has() scope. At a cold start the titlebar renders before the app
mounts, so every subtree mounted later inherited that mark, and each DOM
mutation (a keystroke's textarea update, a streamed token, a row
mounting while scrolling) walked up to <body> and invalidated every
:has() rule over the whole document. Unsloth Web has no custom titlebar,
and a reload mounts the app before the first style pass, so only a
freshly started desktop app showed it.

The titlebar now watches the body's direct children and marks itself
while an open viewport backdrop is among them. Matching is unchanged:
only direct children of the body count, stacked modals keep the dim
until the last one starts closing, and panel-local overlays and menus
never dim it.

Unsloth Desktop on Windows (WebView2 154), cold start, long thread:
style recalc per keystroke 21.6-33.4 -> 0.4-0.5 ms (3,400 -> 5
elements); a 4 s wheel scroll with the thinking box expanded spent
831 -> 95 ms in style recalc, frame p99 33.3 -> 4.3 ms.

* Chat: scroll the thread on the compositor below 1.5x device scale

Below 1.5x device scale Chromium keeps LCD text in preference to
composited scrolling, so it scrolls an overflow container on the main
thread unless the container's scrolling background is known to be
opaque. The thread viewport is transparent. On 100% and 125% Windows
displays, and in Unsloth Desktop wherever its own zoom sits on such a
display, every scrolled frame repainted, re-layerized and re-rastered
the visible thread.

Below that scale the viewport now paints the page background itself.
Chromium only counts a solid background as scrolling contents with an
auto gutter, so the stable both-edges gutter becomes an always-shown
bar plus a transparent start border of the same width. Layout and text
stay pixel-identical, LCD text included. The flag divides the desktop's
page zoom out of the pixel ratio, since Chromium decides on the
display's own scale. 1.5x and above is unchanged, and the docked
browser chat keeps its rounded surface.

Unsloth Desktop on Windows, long thread with the thinking box expanded,
4 s fast wheel scroll at 1x with 150% interface zoom: scroll latency p99
29.2 -> 13.1 ms, main thread 764 -> 496 ms, compositor 253 -> 136 ms,
GPU 321 -> 76 ms, raster 90 -> 15 ms. Unsloth Web in Edge at 1x: main
thread 593 -> 512 ms, GPU 277 -> 94 ms, layerize 85 -> 26 ms, with
identical screenshots.

* Chat: write the corner insets to their consumers, not <html>

Dragging the Run settings panel restyled the whole window on every
frame. The panel's ResizeObserver wrote --studio-chat-settings-inset on
<html> as the edge moved, and a custom property set on the root is
inherited by every element, so each frame Chromium recomputed the style
of the entire document (about 9,000 elements on a long thread) only for
the toast column and the stacked overlay cards to read it. The browser
panel wrote --studio-browser-page-inset the same way.

Both insets now go to the elements that read them: the toaster and the
two overlay rails register themselves, take the current values when
they mount and receive every later write. Nothing is written to <html>.

Unsloth Desktop on Windows (WebView2 154), long thread with the thinking
boxes expanded, a 3 s drag of the Run settings edge: style recalc per
frame 8,997 -> 188 elements at p90, frame p90 87.5 -> 17.6 ms.

* Chat: keep a closed floating monitor out of panel drags

The floating monitor follows the painted width of the Run settings
panel and of the sidebar through ResizeObservers, so it can stay clear
of them. Both stayed attached while the monitor was closed, so every
frame of either drag re-rendered a monitor that was not on screen. They
now attach only while it is open, and attach again when it opens.

* Chat: key the table toolbar rules on the toolbar, not its position

The table toolbar's buttons were styled through
`[data-streamdown="table-wrapper"] > div:first-child:not(:last-child) >
.relative` and `... > button`. A :first-child in an ancestor compound
makes Blink file the subject's features, `.relative` and `button`,
under :first-child invalidation. Every modal and menu inserts a focus
guard at the start of <body>, which changes #root's :first-child state,
and that then invalidated every `.relative` and every button in the app.

The rules now select the toolbar's children directly. The table's own
box holds only the <table>, so they match the same elements, and the
margin rule that does need the position keeps it on its subject.

* Chat: test the parent before a child's position or sibling

When a thread contains math, KaTeX's MathML loads Chromium's MathML
stylesheet, whose `mmultiscripts > mprescripts ~ :nth-child(2n+1)` puts
a whole-subtree set into the universal sibling invalidation set. From
then on, inserting or removing a child anywhere but at the end of a
parent restyles that parent's entire subtree, if the parent was ever
flagged for sibling rules. A selector with a featureless subject is
tried against every element, so `.aui-branch-chevron-btn + *`,
`.aui-branch-chevron-btn + :not(...)` and
`:where(.aui-assistant-message-content > .h-0 + *)` flagged every
parent in the document, <body> included, and
`:where(.aui-assistant-message-content > :not(:first-child))` made every
element track :first-child. Tailwind's space-* and divide-* utilities
did the same for :last-child through `:where(.X > :not(:last-child))`,
and with the thread's trailing-margin chain that made appending to any
container restyle its previous last child's whole subtree.

The branch picker's rules now name the count between the chevrons by
its class, and the message rules check the parent first inside
:where(), keeping zero specificity. A small Vite transform rewrites
Tailwind's `:where(X > :not(:last-child))` to
`:where(X > *):where(:not(:last-child))` in dev and build alike: the
same elements and the same zero specificity, but Chromium matches simple
selectors left to right, so it only checks the position of children of
X. The computed styles of every affected element are unchanged.

Unsloth Desktop on Windows (WebView2 154), long thread with math, one
element inserted and removed at the start or middle of <body>, #root,
the app shell, <main>, the thread root or the viewport: 2,400-2,700
elements restyled, 25-36 ms -> 1 element, 0.2 ms. Appended at the end
of the app shell's containers: 2,400-2,600 -> 1-2. Radix focus guards
(every modal and menu) and tooltip and popover portals are such inserts
and removals.

* Chat: keep the desktop sidebar's edge strip mounted across pins

The desktop sidebar's edge strip returned null while the sidebar was
pinned, so every pin and unpin, Ctrl+B included, inserted or removed a
child between the sidebar and <main>. <main>'s
`peer-data-[variant=inset]` rules flag their parent for sibling
changes, and with math on the page Chromium then restyles everything
below that parent, which is the whole app. The strip's container now
stays mounted and only its handle comes and goes.

Unsloth Desktop on Windows (WebView2 154), long thread with math,
Ctrl+B: style recalc 8,950 -> 175 elements, 65-72 -> 4-7 ms, longest
task 111-126 -> 46 ms.

* Chat: end a panel drag that loses pointer capture

A resize drag lays a full-window overlay over the app to own the cursor
and tears it down on pointerup or pointercancel. If the capture was lost
without either, the overlay stayed and swallowed every click until a
reload. Losing the capture mid-drag now ends the drag like a cancel; a
normal pointerup has already ended it by the time its capture goes.

* Chat: scan for overlays over a native page without walking the thread

While a native browser page is open, a check every 300 ms (and on every
browser store change or new body child) looks for menus, dialogs and
toasts over it, and for the docks that shorten it. Both lookups were
document-wide querySelectorAll calls, so on a long thread each check
walked every element of every message: 2.7 ms for the docks and 4.6 ms
for the overlays on a 61,000-element thread, and the check came to
10.6 ms, a dropped frame or three at 240 Hz every 300 ms.

Nothing that can cover a native page renders inside the thread's
viewport: menus, dialogs, tooltips and the image zoom portal into
<body>, toasts render from the app shell and the page's own covers live
in the browser panel. The lookups now run the same native queries on
everything except the viewport's subtree, which still finds the
full-view dock that encloses it.

Unsloth Desktop on Windows (WebView2 154), long thread, Wikipedia in the
browser panel: the check 10.6 -> 0.56 ms.

* Chat: let the browser page yield to its snapshot while the split is dragged

The split between the chat and the browser pins the page at its size
while it is dragged, so it lays out once on release. A native page is
not clipped by the DOM and was not told about the drag, so it kept its
old bounds the whole time: shrinking the panel slid the chat under the
page, growing it opened a gap.

The split now holds the same drag overlay the sidebar and Run settings
handles use, moved to its own module. Its slot already counts as
covering a native page, so for the length of a drag the page shows the
snapshot it shows under a menu, which the DOM clips with the panel, and
comes back at its new size on release.

* Chat: keep a split drag from restyling and re-measuring the thread

react-resizable-panels sets pointer-events: none on both panels for
the length of a drag. The property is inherited, so pressing and
releasing the split each restyled the entire thread, about 6,000
elements and 40-60 ms. The thread panel's wrapper now sets
pointer-events: auto itself, as the browser side's surface already did,
so the change stops there; the drag overlay keeps hover and clicks off
the content as before.

Thinking boxes re-measure every row 150 ms after their width stops
changing. A drag that paused, or met a panel's limit, did that in the
middle of the drag, 20-45 ms each and again after release. They now
wait for any panel drag to end first.

Unsloth Desktop on Windows (WebView2 154), long thread with the thinking
boxes expanded, Wikipedia in the browser panel, a 3 s drag of the split:
long animation frames 6 (400 ms) -> 0, frames over 33 ms 16 -> 0, frame
p99 33.4 -> 20.9 ms, largest style recalc 6,994 -> 785 elements,
drawn-frame gap median 27 -> 17 ms. Unsloth Web in Edge, same drag:
longest task 57.6 -> 9.9 ms, worst frame 58 -> 17 ms.

* Chat: keep the browser page below its toolbar while the split is dragged

The page was pinned with top: 0 inside the panel's body, so for the
length of a split drag it covered the address bar and the bookmarks bar
in both Unsloth Desktop and Unsloth Web. It is now pinned at the offset
it had, under the toolbar.

* Chat: keep a slider drag inside its own row

Every slider in Run settings and in the model picker's configure page
wrote its owner's state on each pointer move. A Run settings slider
wrote the chat runtime store, which the whole chat page subscribed to,
and a picker slider wrote the configure page's draft config, so each
move re-rendered the entire page around the slider.

A drag now moves its row's thumb and number box on a local draft
(useSliderDraft) and hands the owner one value when it ends. Keyboard
steps and typed values still commit as they go. Labels such as Off, Max
and Auto take a format callback instead of a fixed displayValue, so
they follow the thumb. What an owner shows beside its slider follows
the drag through onDraft: the VRAM Budget advice and the music editor's
extend tail. The context warning moves into the Context Length row, and
the MLX Context Length box shows the dragged length instead of a dash.

Each move still renders synchronously, as the store write it replaces
did, so the thumb is drawn in the frame the pointer moves. The drag
commits its last move when pointer capture ends, so a release right
behind a move commits that move, and a cancelled touch keeps the value
it reached, as before. The memory estimate and the VRAM Budget save
follow once per drag instead of once per move.

Unsloth Desktop on Windows (WebView2 154), 3 s drags. Temperature in
Run settings: pointermove handler p50 3.41 -> 0.46 ms, max 11.1 -> 0.70
ms, DOM mutations 6,715 -> 1,966, frame p90 8.2 -> 4.2 ms. Context
Length in the model picker: p50 4.48 -> 0.52 ms, max 15.4 -> 0.77 ms,
mutations 6,894 -> 1,276, frame p99 12.5 -> 4.3 ms.

* Chat: re-render only the Run settings panel for a sampling change

The chat page subscribed to the whole sampling params object, directly
and through useChatModelRuntime, though it reads only the selected
model from it. Every change to temperature, top P, max tokens and the
rest, whether a drag's release, a keyboard step, a typed value or a
preset, re-rendered the chat page's top component and its 600-odd
hooks. Both now subscribe to the checkpoint alone, and the Run settings
panel reads and writes params from the store itself instead of through
props.

Unsloth Desktop on Windows (WebView2 154), six Temperature keyboard
steps: chat page re-renders 6 -> 0, key handler p50 2.8 -> 2.3 ms, max
3.9 -> 2.8 ms.

* Chat: keep a model picker slider drag from re-laying out the page

The model picker's configure page scrolls inside its popover and, like
the rest of the app, draws the custom ::-webkit-scrollbar. With an
auto scrollbar gutter, Chromium re-checked that scrollbar whenever
anything inside the scroller was laid out, and relaid out the whole
configure page twice a frame for it ("Scrollbar changed"). A slider
drag lays the page out on every move, so every frame paid for the
entire page. The Run settings sliders never did, because that
scroller already reserves a stable gutter.

The configure page's scroller now reserves its gutter as well. Nothing
moves while the page scrolls, since the scrollbar already takes that
space. A page short enough not to scroll keeps an empty 8 px gutter,
and toggling Advanced settings no longer shifts every row by the
scrollbar's width.

Unsloth Desktop on Windows (WebView2 154), long thread behind the
picker, Context Length drag: layout per frame 0.58 -> 0.29 ms,
pre-paint 0.21 -> 0.07 ms, frame time p50 4.17 -> 3.18 ms, move to
drawn frame p90 5.10 -> 4.07 ms and p99 6.01 -> 4.36 ms, level with
the Run settings sliders (3.95 and 4.35 ms).

* Chat: fix review findings in thinking code, streaming, menus and zoom

A review of this branch against main found ten regressions it
introduced. Each is fixed here and measured against main and against
the branch before this commit.

- Long code blocks in thinking: every row of a fence formed one mount
  group, so opening or streaming a long fence mounted, coloured and
  laid out all of it at once, and every streamed update asked for the
  colours of every line again. Groups now stop at the chunk budget,
  splitting only where a line starts. A group that starts mid-fence
  gets no leading line break, a group the fence has moved past keeps
  the source it finished with, so only the tail asks for colours while
  streaming, and the worker keeps one copy of a source its groups
  share. 500K-character saved fence: 221,543 -> 4,938 elements, worst
  frame 2.1-2.5 s -> under 0.1 s (main 1,603 elements). 900 lines
  streamed: frames over 50 ms 31-36 -> 2, long frames 3.4-3.8 s ->
  0.3-0.4 s (main 1 and 0.3-0.4 s); 3,000 lines: 51 s -> 0.38 s (main
  0.39 s). WebKit p95 frame 218-439 -> 41-57 ms.

- Menus: the thread's wrapper set pointer-events: auto at all times,
  so the click that dismissed a modal menu, select or context menu also
  pressed whatever lay under it in the thread, and hover leaked through
  while it was open. The override now applies only while
  html[data-panel-resizing] is set, which is when the panel library
  blanks the panels. The dismissing click lands on <html> again, as on
  main, and a split drag still restyles about 27 elements (main
  restyles the whole 60,000-element test thread).

- Streaming text: the measured render cost included the wait for the
  next frame, so a 60 Hz screen rendered every third frame. The cost is
  now the commit itself, and the 16 ms floor is checked against frame
  timestamps instead of a timer. Updates per second 19.7 -> 59.4 in
  Chromium, 19.5 -> 59 in Firefox, 16 -> 55 in WebKit (main 59.4, 59.2,
  56).

- Font changes: skipped code blocks and thinking chunks kept the height
  they had at the old font size, so scrolling back to them jumped. A
  change of UI or code font size, font family or the browser interface
  scale now renders them once at the new metrics, as the desktop zoom
  already did, on every engine. Scroll-in jump 6,623-6,640 -> 0 px for
  the code font size in Chromium, Firefox and WebKit, 3,136 -> 0 for
  the UI font size, 3,923 -> 0 for the interface scale, 186 -> 0 in a
  thinking box.

- Autoscroll: the first pin of a freshly opened thread waited until the
  thread overflowed, however long that took, so a card opened by hand
  in a short chat snapped the view to the bottom. It now resolves at the
  first frame that shows messages. Reopened threads still land at the
  bottom.

- Search images: a running message passed no texts to subject image
  placement, so an earlier part showed a card a later part had already
  placed. The texts pass again whenever the message has search images.

- Highlight worker: the idle teardown of a stalled worker left the
  state stalled, so long fences stayed uncoloured for the rest of the
  session. It now resets to untested.

- Forced colours: a contrast theme painted the low device scale rule's
  transparent 10 px border as a solid bar. The rule now skips forced
  colours.

- Find in page: stepping onto a match whose text a fence's line window
  had just swapped read a stale index and revealed nothing. Next and
  previous now rebuild it first. Steps that missed at 60 ms intervals:
  23/40 -> 0/40 in Chromium, 31/40 -> 0/40 in Firefox and WebKit, as on
  main.

- Pinch zoom: Unsloth Desktop on Windows and Linux armed its wheel
  listener only after a Ctrl keydown, and a touchpad pinch sends
  Ctrl+wheel without one. The desktop listens all the time again, as
  main does. Unsloth Web keeps the narrower listener.

tests/studio/reasoning-transcript-checks.js and
smoke-reasoning-transcript-main.tsx are back to main's version. Main's
first check bounds a saved trace's mounted text below 20,000
characters, which this branch's transcript does not do by design: rows
stay mounted and the engine skips them. Without that one check the
branch passes the other 15, with and without row containment.

general-settings-token-sync.test.ts faked the update banner module
without the audio.cpp functions #12893 added, so all nine of its tests
failed on main. The fake now has them.

* Chat: fix thinking code seams and colours, fast-display cadence and the Windows gutter

A second review of this branch found more regressions it introduced.
Each is fixed here and measured against main and against the branch
before this commit.

- Long code blocks in thinking: since groups stop at the chunk budget,
  a fence renders as several blocks. A group that ended on a blank
  line lost it, because a trailing line break draws no line, and a
  copy across the seam dropped it in Chromium and WebKit. Firefox's
  copy added a blank line between every two <pre> blocks. Groups are
  now block elements that are not <pre>, carry the code font
  themselves, and end with the line break when the next row starts a
  line. 2,400-line fence with a blank line every 7 lines: 2,395 ->
  2,400 lines drawn, copies across seams exact in Chromium, Firefox
  and WebKit, line pitch across seams unchanged.

- Thinking code colours: a group the stream had moved past asked for
  colours with the source it finished with and got the throttled
  approximation with a plain tail, and the next group's request on the
  same cache entry dropped its queued refresh. Nothing asked again, so
  those lines stayed plain until the thread was reopened: 460-604 of
  2,400 lines after a followed stream, 1,432 after a detached one
  (main and the branch before the previous commit: 0). Every group but
  the growing tail now asks the worker for exact colours, which never
  cancels the tail's refresh, and a group keeps its own source only
  while its fence grows, so a finished fence shares one source again
  and the worker keeps one copy of it. 0 plain lines in Chromium,
  Firefox and WebKit.

- Streaming text cadence: frame times come in whole frames, so the
  16 ms floor rounded up to the next frame and a 75, 90 or 144 Hz
  display rendered every second or third frame (37.5, 45 and 48
  updates per second). The floor now allows half of the measured frame
  interval: 75, 90 and 72 per second, still 60 at 60, 120 and 240 Hz,
  and the spacing by render cost is unchanged. WebKit at 60 Hz: 56.8
  -> 59.5 per second.

- Idle work: the branch made the shared scheduleIdleTask wait for
  300 ms without input where requestIdleCallback is missing, which
  also delayed the settings prefetch, the hub readme and markdown
  previews in Safari and Unsloth Desktop on macOS and Linux (120 ms ->
  up to 250-400 ms). scheduleIdleTask is main's again, and the wait
  moved to scheduleQuietIdleTask, which only the chat's highlighting
  uses.

- Windows gutter: the native thin scrollbar keeps its device size
  under a browser zoom as it does under the desktop zoom, so in
  Unsloth Web the gutter stayed 10 px while the bar was 8 px at 125%
  and 12.5 px at 80% (Windows Chrome). The gutter is now the measured
  bar, measured again whenever the pixel ratio changes, and
  --studio-interface-zoom is gone. Gutter, start border and bar match
  at 80, 100 and 125% on 1x and 1.5x displays.

- Safari 16, and so Unsloth Desktop on macOS 11, has no
  contain-intrinsic-size, so the zoom scale probe measured 0 and was
  inserted again on every measure of a long code block. It is skipped
  there.

tests/reasoning-code-groups.test.ts drives the real highlight worker
and fails on the previous commit.

* Chat: fix thinking code anchoring, find in long code, worker recovery and fast-stream following

A third review of this branch found more regressions it introduced.
Each is fixed here and measured against main and against the branch
before this commit.

- Reading position in thinking code: code groups became <div>s in the
  previous commit, so the reading anchor no longer saw their text.
  With only code on screen, narrowing the window from 1280 to 520 px
  moved it 2,559 px in Chromium, Firefox and WebKit. Code groups count
  as passages again: 0.4 px.

- Code font size: a custom size only reached the inner <code>, so the
  block kept the default line spacing (18.89 px at a 10 px setting,
  15.5 px on main). The size rule covers code groups too, as a
  separate selector so existing matches keep their specificity.

- Highlight worker stalls: a caller turned away while the worker was
  late cancelled the idle teardown, so a stalled worker could stay
  stalled for good. Groups whose requests were failed, or that mounted
  during the stall, never asked again, and fences too long for the
  main thread stayed plain until reopened. Turning a caller away no
  longer touches the teardown, and every mounted group asks again when
  a late worker answers.

- Find in long code blocks: find draws its highlights on live ranges,
  and the 240-line window swapped a line's text for token spans as the
  reader moved, so highlights vanished until the index was rebuilt
  (21 of 60 after a scroll, and the active match for about 320 ms
  after Enter; main 0). While the find bar is open a long block
  renders every line, as it does for print, and windows again once
  the bar closes.

- Worker transfers: each group of a long thinking fence sent the
  whole fence to the worker, 28 times for a 250K character stream
  (about 3.6M characters). A group's first request now edits the
  source the worker already holds for that fence: one full transfer,
  as on main.

- Following fast streams: code rows were estimated with the prose line
  height (21.3 px for 18.9 px code lines) and a 16 px footer (12.25 px
  rendered), so mounting them shrank the transcript under a reader
  pinned to the bottom. The browser clamped the scroll position and
  the autoscroll took it for the reader scrolling up. Estimates now use
  the code block's measured line height, font size and footer, and the
  transcript no longer shrinks while it streams. At 512 characters
  every 12 ms the stream stayed followed in 16 of 16 runs (lost in 3
  of 8 before). Firefox and WebKit were unaffected.

- Sliders: a row that unmounts mid-drag, such as the model picker
  closing on Escape, commits the dragged value, as main's live writes
  did.

- Continue on a reasoning-only reply uses main's whitespace check
  again, which the backend guard mirrors, and the zoom comment in
  provider.tsx is back above the function it describes.

tests/highlight-worker-stall.test.ts drives the hook through a stall
and through a fence split across groups, and fails on the previous
commit.

* Chat: fix checkpoint skips, thinking trace reading position, highlight sources and two-finger sliders

A fourth review of this branch found more regressions it introduced.
Each is fixed here and measured against main and against the branch
before this commit.

- Checkpoint saves: they were skipped whenever the thread had a durable
  run, including one the server had named earlier and that a stalled
  or failed recovery left behind. A Continue, an image turn or an
  external provider then streamed with no saves at all, so closing the
  tab lost the reply (main saved every 8 s, capped at 30 min). Saves
  are now skipped only while this tab streams its own admitted run,
  which the server keeps. The cap is unchanged.

- Reply below a thinking box that is still filling: when only the
  box's placeholder was on screen, nothing held the reply, and mounting
  rows there moved it 131 px in Chromium and WebKit and up to 919 px
  in Firefox (main 0.4 px). A box that starts above the viewport and
  ends inside it now keeps its bottom edge in place: 0.6 to 1.1 px in
  all three.

- Rewrites that are not an append, such as a closing think tag split
  across two chunks: every row got a new key, the mount started over
  and the reading position was lost (6,920 px on a static rewrite,
  main 16 px; 48 paragraphs on a stream, main 18). The reader's row is
  now found again by its place in the trace and mounted with the rows
  under it before the next paint: 0 to 1 px in Chromium, Firefox and
  WebKit. A reader following the stream is unaffected.

- Highlight sources: after a failed reply the worker drops that
  client's source, but the hook still sent the next request as an edit
  of it, so the fence stayed plain. A failed reply now forgets the
  client's sources, as unmount does.

- Sliders: lifting the first of two fingers ended the drag and the
  second finger's moves were ignored (main followed it). The drag now
  ends when the last pointer is released and commits once.

- The Windows hover scrollbar rules were already covered by the
  important universal rule, so they are removed, and a stale comment
  about smooth scrolling at run start is gone.

tests/run-checkpoint-live-run.test.ts, the new cases in
reasoning-mount-controller, highlight-worker-stall and slider-draft
cover these and fail on the previous commit.

* Chat: fix near-end thinking rewrites, long code block hops and slider saves on unmount

A fifth review of this branch found more regressions it introduced.
Each is fixed here and measured against main and against the branch
before this commit.

- Rewrites near the end of a thinking trace: after a rewrite that is
  not an append, such as a closing think tag split across two chunks,
  the trace mounts again from its first rows and the rest is held by
  estimates that are lower than the rendered height. A reader near the
  end was clamped to the bottom, the hold measured from the transcript
  and missed it, and autoscroll started following again (-6,949 px at
  97% of a static rewrite, -10,985 px at 90% of a stream; main +16 px
  and about 5 paragraphs). The reader's row now keeps its place on
  screen across the restart and across the commit that mounts the rows
  under it, where Chromium's scroll anchoring on the reply below moved
  the page again. 0 to 1 px in Chromium, Firefox and WebKit, and a
  reader following the stream still follows.

- Finding the reader's row after such a rewrite: a stream holds a code
  fence as code rows of 16 lines, while the full parse after the
  rewrite can hold the same fence as one prose row, and the paragraph
  after it then starts one character later. The row was not found and
  the reader was lost. The row is now matched by its place, or by the
  row that holds its first character, and when a fence changes between
  code rows and a prose row the passage count carries the fence's
  earlier lines. A reader inside a 120-line fence: -1,152 px before,
  -22 px now in Chromium and Firefox.

- Long code blocks: content-visibility sat on the code body, which
  scrolls horizontally. Skipped, it lost its scrollbar in Chromium and
  its padding in Firefox, so a block over 240 lines moved the page
  10 px when it entered or left the viewport, and the blocks above the
  reader moved it again when the find bar opened or closed (main 0).
  Containment now sits on a plain box around the body, and the body is
  not measured while it is skipped. 0 px in all three, with the same
  layout cost on resize and scroll.

- Sliders: when a subtree unmounts, a parent's passive cleanup runs
  before its children's, so the VRAM Budget row flushed before the
  slider committed a drag cut short by Escape, and the value was saved
  400 ms later on a row that was gone. The slider now commits in a
  layout cleanup, which runs before any passive one.

- Highlight worker: an edit of a source the worker no longer holds is
  answered with a failure instead of being applied to an empty string.

- Cleanup: a stale autoscroll comment is corrected, the live run check
  and the part separator each have one definition, and widenRows and
  REASONING_ROW_CONTAINMENT_GLOBAL, used only by tests or not at all,
  are removed.

New cases in reasoning-mount-controller, reasoning-code-groups and
slider-draft cover these and fail on the previous commit.

* Chat: show thinking code as plain text and stop restyling growing blocks

Thinking code is now plain text on purpose. Colouring it kept the
frame rate but doubled main-thread work while a trace streamed, and
the thinking box should cost as little as possible. Code in answers
is still coloured.

- Thinking box: code rows in long traces are plain text, and fences
  in short traces and in the trace's prose rows use the plain code
  block. No grammar work and no worker requests. The copy button and
  language label stay. A 60K-character thinking code stream in
  Chromium now takes 7.2 s of main-thread work, against 16.3 s when
  coloured and 26.6 s on main.

- --unsloth-fence-height and --unsloth-chunk-estimate are registered
  as non-inherited. Each write restyled every element in the code
  block or thinking chunk, so a long streamed block paid 20 to 45 ms
  per line. Streaming 200 lines onto a 3,000-line block: style recalc
  6.2 s before, 0.08 s now (main 0.07 s).

- Removed with the colouring: the thinking highlight hook and its
  helpers, reasoning-line-tokens, the worker's base and exact
  requests, and FenceLine's inline option. The worker still colours
  long answer fences.

- The highlight worker fails only the listeners that were waiting
  when it stalled.

- general-settings-token-sync.test.ts now matches upstream's fix.

Tests for the removed code are dropped. New cases pin the plain
thinking code and the two registrations.

tests/studio/reasoning-transcript-checks.js now fails its check that
less than 20,000 characters are mounted 600 ms after opening a saved
trace. The first render is unchanged (3,058 characters). Plain rows
only mount faster, about 40K by 600 ms, and the previous commit also
passes 40K by 800 ms.

* Chat: fix thinking code selection, equation numbers and streaming pauses

Another review of this branch found more regressions it introduced.
Each is fixed here and compared against main and against the branch
before this commit.

- Selecting thinking code while it streams: a code block in a short
  trace and each code row in a long one were a single text node, so
  every update rewrote it and cleared the selection. 16 characters
  selected dropped to 0 on the first update in Chromium, Firefox and
  WebKit, while main kept them. Each line and each line break is now
  its own text node, so only the growing line changes. This also
  fixes a 40 px jump when a trace passes 16,384 characters with the
  first visible line at the end of a code row. The saved reading
  position could span a line break that the transcript does not join,
  and now stays inside one line (0.3 to 0.5 px).

- Numbered equations: dropping the size container from the thread
  root also dropped its style containment, which kept KaTeX's
  equation counter from running across messages. A fourth message
  with one equation showed (4). The root now has contain: style and
  every numbered equation reads as it does on main, in all three
  engines. A WebKit width change stays at 1 ms (83 ms with the old
  container).

- Streaming pauses: the render cadence timed everything from the frame
  to React's commit, so an unrelated long task counted as render cost.
  After a 400 ms task the text paused another 817 ms (main 20 ms). The
  render now runs in a task posted after the frame and is timed on its
  own, and the text resumes after 18 ms. On a 200K character answer,
  main-thread time goes from 75.3% to 77.0%, since other work no
  longer holds renders back.

- Background tabs: the pin to the bottom waited for a frame, which a
  hidden tab never draws, so a reply that ended there showed 8,376 px
  above the bottom for one frame on return. While the document is
  hidden, a change now pins at once, as it does on main.

- Long answer code: a block over 20,000 characters waited for the
  worker when it finished, leaving its last lines plain for up to
  300 ms in Chromium and 1 s in WebKit. When 512 characters or fewer
  are left uncoloured, the main thread colours them at once, in 1 to
  2 ms in WebKit. Longer tails still go to the worker.

- Run settings: a slider that unmounted mid-drag in the same update
  that changed the model committed from its last render and switched
  the model back. Commits now read the current settings from the
  store.

- Chat split: a window blur ended our side of the drag while the panel
  library kept dragging, so the final width was not saved and the
  browser panel could close. The drag now ends when the library's
  does.

- Cleanup: removed the transcript's print upgrade call and its export,
  which had nothing left to upgrade, the unread data-reasoning-row
  attribute and the deferred marker on plain thinking blocks.
  child-selector-order.test.ts no longer adds three unused rules to
  the production CSS. An upstream comment about Streamdown's
  scheduling is back above the constant it describes.

Tests pin each change, and all 11,148 frontend tests pass.

* Chat: fix thinking box streaming cost, slider commits and the split press

Another review of this branch found regressions it introduced. Each is
fixed here. The performance changes are measured on production builds
against main and against the previous commit.

- Thinking code while streaming: the previous commit made each line of
  a code row its own text node, and every update re-rendered every
  mounted row. Code rows are now memoised, so only the growing row
  renders. A 1M character code trace in Chromium: 18.2 s of main
  thread before, 6.6 s now (main 32.6 s). Firefox's slowest 5% of
  frames: 83 ms before, 17 ms now (main 17 ms).

- Long mixed traces: every update estimated the height of every row
  again and re-rendered every mounted chunk. Estimates are now kept per
  row, and a closed chunk renders again only when one of its rows, its
  estimate or its slices change. A slice that is still being written
  says so, so the copy button of a fence that ends mid-stream is
  enabled with the whole source. A 500K character trace in Chromium:
  13.1 s before, 8.7 s now (main 9.1 s), and the stream ends 0.8 s
  sooner. Firefox's slowest 5% of frames: 83 ms before, 17 ms now
  (main 17 ms). The rendered DOM is identical to the previous commit,
  and a selection in streaming code is still kept.

- Sliders that other text reads: commits wait for release on this
  branch, so the music Variations reload notice and the Edit page's
  Generate hint and "How edits apply" text showed the old value during
  a drag. These three sliders commit on every move again, as on main.
  None has more than 16 positions.

- Sliders and outside changes: a release wrote the dragged value over a
  change that arrived after the last move, such as a model load while
  the thumb was held still. The release now commits only when the value
  is still the one the last move saw, so the newer change wins, as on
  main.

- Chat split: a press added the drag overlay, which a native browser
  page counts as covering it, so a plain click hid the page behind its
  snapshot and showed it again. The press now only marks the drag,
  which keeps the thread's pointer events while the panel library
  blanks both panels, and the overlay comes once the pointer has moved
  4 px.

- Streaming code blocks windowed their lines whatever the fence mode,
  so "off" no longer restored main's rendering. They follow the mode
  again, as on main.

- The blocking screen, find bar and browser overlay flags are set in a
  layout effect, so a startup screen raised by a background update no
  longer paints for a frame under the find bar.

New cases in reasoning-mount-controller, slider-draft,
panel-drag-overlay, code-fence-mode and interface-zoom cover these. All
11,153 frontend tests pass.

* Fix frontend interaction and cache regressions

Preserve slider commits after input edits and capture loss. Read the latest reasoning code for copy and download actions.

Scope scrollbar gutter updates, bound find refresh delays, and invalidate shared thread reads after settings updates. Skip fence measurements while content is hidden and clarify print behavior.

* Fix reading position after thinking segment rewrites

Detect rewrites across all thinking segments and preserve the reader's position. Add regression tests for rewrites, appends, and segment removal.

Restore the upstream print comment with the budget limit and fix the rollback test stub indentation.

* Fix viewport media gating and desktop wheel handling

Keep breakpoint gates current during resize and reload. Handle comments
in selector lists and resolve the boot script against Vite's base path.

Keep desktop zoom wheel events cancellable when Ctrl keydown is missed.

* Fix transcript resize anchoring and panel subscriptions

Keep the answer in place while thinking rows are remeasured after a width change, accounting for native scroll anchoring and fractional rounding.

Share panel width listeners until the last subscriber unmounts. Remove redundant wheel listener cleanup and update the related tests.

* Parse viewport media gates with PostCSS

Replace the hand-written CSS scanner in the viewport media plugin with
postcss and postcss-selector-parser. The generated CSS and boot script
are unchanged.

Leave a media block as written when one of its selector lists does not
round-trip through the parser, so invalid selectors stay invalid. Leave
a stylesheet that cannot be parsed ungated with a warning instead of
failing the build. Return a source map when build source maps are on.

Add postcss and postcss-selector-parser as dev dependencies at the
versions already in the lockfile.

* Keep desktop shell work off the chat and its scrollbar

Watch modal changes for menu shortcut availability only where the
native app menus exist, on macOS desktop. Other desktop builds no
longer rerun every shortcut claim on each body mutation.

Read the sidebar width from its own store instead of the sidebar
context, so a width change no longer re-renders every context
consumer.

Start the chat scroller below the custom titlebar so the titlebar no
longer paints over its scrollbar, and reset the offset in the docked
chat.

* Keep desktop wheel scrolling on the compositor thread

On Windows and Linux desktop every wheel scroll in the chat waited on
the main thread, so painting and styling maths during a scroll dropped
frames. Two things caused it.

The interface zoom kept a blocking wheel listener on the window at all
times. Use the same gating as the browser build: arm the blocking
listener only while Ctrl or Meta is held or the pointer is over a zoom
scope. A passive listener arms it when a Ctrl+wheel arrives after a
missed keydown, so only that first notch scrolls, and a plain wheel
disarms a listener left armed by a missed keyup. macOS keeps the
listener armed because a pinch reaches its zoom scopes without a
keydown.

The pinned sidebar edge decoration in the custom titlebar was a box
from the sidebar edge to the window edge at full height, laid over the
chat, so the compositor could not resolve the scroll target. Draw the
edge below the corner as a border on a zero-width element instead. The
screenshot matches in both themes.

In a long maths chat with both thinking boxes expanded, scrolling up
from the bottom dropped 40% of frames before and 0 to 1% after.

* Pin the desktop titlebar contract to the zero-width sidebar edge

17806ce539 drew the pinned sidebar edge below the corner as a border on
its own zero-width element instead of a full-height box over the chat,
but the desktop reliability contract still pinned the old box, so
test_expanded_titlebar_button_and_corner_match_sidebar_edge failed.
Pin the corner border and the separate edge element instead.

* Fit upstream's dark menu glow, continuation and compaction changes

Adaptations of this branch to the code the origin/main merge brought in.

- Dark menu glow (#13013): the dialog and picker context rules used
  `body > dialog ~ * :is(menus)`. Evaluating that `~` flags <body>, and
  with KaTeX's MathML on the page every later insert at the start of
  <body> (Radix focus guards on each modal or menu open and close)
  restyled the whole page, even after the menu closed. Card dialogs and
  the model picker now hand their surface to the menus opened inside
  them through a context, the menu primitives stamp it as
  data-dropdown-glow, and the CSS keys on that attribute. With the built
  stylesheet, a focus guard insert in dark mode with maths now restyles
  1 element instead of the whole page (7,514), and the menu's glow,
  accent, background and shadow compute identically in the card,
  picker and plain contexts.
- child-selector-order guard: normalise whitespace before splitting
  combinators, and flag a `+` or `~` whose right side names no element
  in any position, including inside :is()/:where()/:not()/:has(). The
  old check only caught two of the three glow rules, by accident of
  their line breaks. upstream's modal-open-restyle test now pins the
  attribute form instead of `~ *`.
- useContinuation: upstream's Gemini replay rewrite read message.content
  on every token of a streaming reply; read it only once the reply is
  no longer running, as the reasoning selector already does.
- Remove compaction-notice-owners.ts and its test: upstream's
  compactionNoticeMessageIds in message-derived.ts is the same walk and
  is what AssistantMessage uses after the merge.
- titlebar-modal-layering: ignore CSS comments when checking for a
  :has() on the document root; upstream's comment names body:has().

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Escape viewport boot script data for HTML safety

Escape HTML delimiters and Unicode line separators after JSON serialization. Preserve the runtime values while keeping the generated external script safe if it is later inlined.

Add regression coverage for script-closing tags, quote and backslash payloads, control characters, Unicode separators, and viewport range queries.

* Render code block line windows and streaming highlights in smaller pieces

Scrolling past a finished code block moved its line window with a plain
state update, so React rendered the new lines and removed the old ones
in one blocking task. That update now runs in a transition when the
viewport kept its size, which lets React yield between pieces. Streaming
fences, the first measure, print, find and resize keep the synchronous
update. On a 4x slower CPU, scrolling the long chat with thinking
collapsed went from 8 to 12 frames over 50 ms down to 4 to 6.

The throttled highlight refresh tokenized every newly completed line of
a streaming fence in one call. When a reply opened its first script
block, the embedded grammar's first use cost 178 ms in a single task.
The refresh now tokenizes line by line for up to 8 ms, shows the rest as
plain text and continues in the next task until it catches up. A
request for exact tokens never gets the partial result. The worst frame
in the answer of a 103K character reply went from 150 ms to 100 ms
(upstream 133 ms), and WebKit frames over 50 ms in that phase from 47 to
18.

Add tests that pin the transition wiring and that force the refresh
into slices with a fake clock.

* Route the reach warm like the render, and scope shared thread reads to the auth session

The reach latch's warm decided with tokenizesOffThread alone, so on WebKit a
settled fence the render sends to the worker was tokenized synchronously on
scroll. Use settlesOffThread there too.

A shared in-flight thread read is now joined only under the auth session it
started in, so a read never answers a later session.

* Colour thinking code once a block has closed and nears the screen

Thinking code stayed plain because colouring the growing block doubled a
trace's main-thread cost. Colour only settled blocks instead: a fence is
plain while it streams, and once its closing delimiter lands (or the trace
ends) and it comes within a screen of the viewport, the highlight worker
colours it once. Results are cached across row remounts, and without the
worker the block stays plain, so no grammar work runs on the main thread.

* Colour thinking code while it streams, only while it is on screen

* Trim comments

* Hold the event loop open across the 1 ms deadline in the thread read sharing test

* Keep following a stream when a clamp, not the user, moves the viewport up

* Raise the startup budget by the long-chat render work's measured growth

* Match the timestamp fixture's branch count to the app markup and wait for the full refresh in the fork test

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
2026-10-12 03:49:48 -07:00
Daniel Han
15b2867e23
Studio: let a project run its tools in an existing local folder (#13353)
Fixes #10873
Fixes #9321
2026-10-12 03:39:29 -07:00
Michael Han
606057403c
Studio: hide the response-details badge without a fade (#13397)
Leaving the More menu's time row while hovering its info badge could leave the badge's grey circle on screen with a blank square over the icon in the desktop app. The opacity fade gave the badge its own compositing layer inside the menu's rounded scroller, and WebKit did not always repaint it once the fade ended. Toggle visibility instead, so a hidden badge is never painted.
2026-10-12 03:22:15 -07:00
Daniel Han
bf10801178
Tests: keep five tests off what the host has installed or downloaded (#13395)
- test_media_model_index_accounts, test_openai_catalog: the media scan walks
  every known HF cache home, including a host HF_HOME and the platform
  default under XDG_CACHE_HOME, which the suite-wide empty hub cache does not
  cover. Point both at nothing for these tests.
- test_training_gated_upstream: the 4-bit row of the pre-detect test reads
  the 4-bit table only where bitsandbytes is usable. Pin that answer; the
  fallback has its own test.
- test_sandbox_linux: _fake_editable now makes its dist-info the only one
  editable_source_roots sees, so a pip install -e in the running
  environment is not granted too.
- test_async_grpo_fast_inference: skip the TRL client round trip when
  VLLMClient has no LoRA adapter calls (TRL < 1.14).
2026-10-12 03:01:37 -07:00
Michael Han
17afa54bf2
Studio: Working... placeholder with a soft glint (#13382)
* Shimmer the Generating placeholder while a reply starts

* Let the Generating sweep show in light mode

* Narrow the Generating sweep and run it every 5 s

* Soften the Generating sweep

* Run the Generating glint on the compositor

* Give the Generating glint a cosine falloff and sine easing

* Trim comments

* Say Working... and run the glint every 3 s
2026-10-12 02:49:00 -07:00
Daniel Han
1ecae8646d
Studio: screen a quoted program path that cmd /c or start launches (#13386)
* Studio: screen a quoted program path that cmd /c or start launches

* Studio: read a relative path in START's program slot as the program

* Studio: keep rooted command lines and START titles out of the program-path check

* Studio: read a %VAR% rooted path as a program path too

* Studio: let START's program slot resolve an extensionless path

* Studio: let cmd /c resolve an extensionless quoted program path too
2026-10-12 02:28:41 -07:00
Daniel Han
20030550f8
Studio: classify the 10 new audio.cpp-gguf folders as needing a newer runtime (#13393)
audio-cpp/audio.cpp-gguf gained AST-AudioSet, CED, FireRedVAD, Hviske-v6,
MicroWakeWord, MossFormer2, Phonon-2, Sherpa-KWS, TF-GridNet and
Voxtral-4B-TTS-2603. The scheduled Model catalog network check fails until
each is classified. None of them load under the pinned v0.9.0 runtime:
nine use families it does not know, and Phonon-2 is a parakeet_tdt
variant whose config only newer runtimes read. List them as unoffered so
the pickers stay as they are.
2026-10-12 02:23:37 -07:00
Michael Han
160cb41fe1
Revert "Studio: keep the composer expand button in the top corner when a file is attached (#13198)" (#13394)
This reverts commit f33fc6ad1d.
2026-10-12 02:00:38 -07:00
Daniel Han
f1eb893836
Studio: add an opt-in setting to read replies aloud automatically (#13357)
* Studio: add an opt-in setting to read replies aloud automatically

* Studio: keep auto read-aloud silent on cut-off replies and hidden chats, stop it on chat switch

* Studio: trim auto read-aloud comments
2026-10-12 01:48:15 -07:00
Daniel Han
1201351b90
Studio: forward llama-server /metrics, /slots, /tokenize and /detokenize (#13354)
* Studio: forward llama-server /metrics, /slots, /tokenize and /detokenize

Owner-only passthrough on Studio's own port, behind Studio auth, with the
child's per-launch key added. Every other method on these paths keeps its
404, slot save / restore / erase stays denied, and /props now reports
endpoint_slots / endpoint_metrics from the child for the owner.

Fixes #11705

* Read the llama-server port once in the passthrough

An unload flips is_loaded before it clears the port, so a request in that
window built http://127.0.0.1:None, which httpx rejects with InvalidURL
(not an HTTPError) and surfaced as a 500. Answer 503 instead.

* Cap /tokenize and /detokenize request bodies

They sit outside /v1 and /api, so MaxBodyMiddleware's prefix list did not
cover them, and keyless inference scope admits both.

* Trim comments in the llama-server passthrough

* Drop two restating comments in the llama-server passthrough
2026-10-12 01:35:45 -07:00
Daniel Han
55ff88ac00
Studio: torch 2.13 for new Linux aarch64 cu130 Python 3.13 installs, existing installs keep their torch (#13380)
* Take the cu130 torch 2.13 route on new Linux aarch64 installs too

* cu130 2.13 route: needs glibc 2.35 and a GIL-enabled 3.13; aarch64 repairs keep only 2.10+

New installs on a host whose glibc is older than the kernel wheels' floor,
or on a free-threaded interpreter (no cp313t torch 2.13), stay on the
default range. Existing installs keep their release as before. A repair on
aarch64 no longer pins a resident torch 2.9, which has no matching
torchvision / torchaudio on the cu130 index.

* Trim comments
2026-10-12 01:23:02 -07:00
Daniel Han
1f20a5f599
Studio: add an embedding device setting for RAG (#13365)
Adds an Embedding device setting (Automatic, CPU, GPU) to Settings > Documents & RAG. It applies to every embedding model and both embedding backends. Automatic keeps the current behaviour, RAG_EMBED_DEVICE still wins when it names a device, and a GPU load that does not fit retries the same model on CPU so indexed documents stay searchable.

Fixes https://github.com/unslothai/unsloth/issues/11768
2026-10-12 01:17:04 -07:00
Daniel Han
2e38770140
Studio: optional detailed Live monitor with GPU load, temperature and power (#13351)
* Studio: optional detailed Live monitor with GPU load, temperature and power

* Studio: gate the GPU detail tiles on an unread host like the other tiles

* Studio: label the Settings GPU tile with the readings it shows

* Studio: carry GPU telemetry into the CUDA inference inventory and hide an unreported load bar

* Studio: honour the CPU load choice in Settings and restore all metrics for an unrecognised saved list
2026-10-12 01:07:28 -07:00