mirror of
https://github.com/unslothai/unsloth.git
synced 2026-10-19 08:54:21 +00:00
5022 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
e4609246eb
|
Studio: run LTX-2.5 on the Video page (#13463) | ||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
dbe40b1ad6 | Revert "Studio: regenerate a chat's title from its latest turns (#13197)" | ||
|
|
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> |
||
|
|
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. |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
10dc580cc7
|
Studio: image-to-video for Wan2.2-TI2V-5B and LTX-2 / LTX-2.3 (#13378) | ||
|
|
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> |
||
|
|
fe1709fe62
|
Revert "Unsloth Studio / Desktop: colour a companion download's bar by model, text encoder and VAE (#13112)" (#13431)
This reverts commit
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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 |
||
|
|
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. |
||
|
|
d6cfacfbfb
|
Keep syntax colour on settled thinking fences in tilde, four-backtick, indented and stopped forms (#13415) | ||
|
|
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 |
||
|
|
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. |
||
|
|
592be14608
|
Studio: Wan2.1 T2V 1.3B and 14B video families (#13376) | ||
|
|
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> |
||
|
|
10bddef22e
|
Studio: opt-in MLX image engine for Apple Silicon (Z-Image) (#13379) | ||
|
|
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 |
||
|
|
27626031e6
|
Studio: judge hosted pre-quant checkpoints before downloading them, and make the int8 fingerprint torchao-version-neutral (#13339) | ||
|
|
d3e21f3ee2
|
Studio MPS: opt-in fp16 compute with fixed headroom for Z-Image (#13375) | ||
|
|
cfe6d9344d
|
Studio: refuse LTX-2.5 instead of loading it as LTX-2 (#13370) | ||
|
|
0464be9dad
|
Studio: sd.cpp on Macs, CPU backend on a paravirtual GPU and no direct conv on Metal (#13369) | ||
|
|
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. |
||
|
|
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> |
||
|
|
15b2867e23
|
Studio: let a project run its tools in an existing local folder (#13353)
Fixes #10873 Fixes #9321 |
||
|
|
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. |
||
|
|
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). |
||
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
160cb41fe1
|
Revert "Studio: keep the composer expand button in the top corner when a file is attached (#13198)" (#13394)
This reverts commit
|
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |