From bf702033a64c6a3226b29ac049771e113021e2ba Mon Sep 17 00:00:00 2001 From: shimmyshimmer <107991372+shimmyshimmer@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:54:45 -0700 Subject: [PATCH 001/170] Setup: clear stale WebView caches on install/update, keep user data The desktop app's WebView caches (keyed by the Tauri bundle id ai.unsloth.studio) hold copies of the previous frontend and keep serving them after an update, so old styles linger even though the code on disk is new. Clear cache-only paths on every install/update via setup.sh and setup.ps1 (both fresh installs and 'unsloth studio update' route through them). Cleared: the HTTP/network caches, CacheStorage, service workers, and GPU caches for the bundle id. Kept: LocalStorage, IndexedDB, cookies, settings, models, and the studio database. macOS: ~/Library/Caches/ and WebsiteData/{CacheStorage, ServiceWorkers,DiskCache}. Linux: XDG cache dir. Windows: EBWebView\Default\{Cache,Code Cache,GPUCache,Service Worker}. Adds tests/sh/test_setup_webview_cache_clear.sh covering both OS branches, XDG overrides, and that user-facing storage survives. --- studio/setup.ps1 | 19 +++++ studio/setup.sh | 33 ++++++++ tests/run_all.sh | 1 + tests/sh/test_setup_webview_cache_clear.sh | 92 ++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100755 tests/sh/test_setup_webview_cache_clear.sh diff --git a/studio/setup.ps1 b/studio/setup.ps1 index c6932121c9..8e15976e8c 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1129,6 +1129,25 @@ if ($script:StudioVtOk -and -not $env:NO_COLOR) { Write-Host " $Rule" -ForegroundColor DarkGray } +# WebView2 caches keyed by the app bundle id hold copies of the previous +# frontend and can keep serving it after an update (old styles linger). +# Cache-only paths: Local Storage, IndexedDB, cookies, settings, models, +# and the studio database are untouched. +if ($env:LOCALAPPDATA) { + $wvDefault = Join-Path $env:LOCALAPPDATA "ai.unsloth.studio\EBWebView\Default" + $wvCleared = $false + foreach ($wvSub in @("Cache", "Code Cache", "GPUCache", "Service Worker")) { + $wvPath = Join-Path $wvDefault $wvSub + if (Test-Path -LiteralPath $wvPath) { + try { + Remove-Item -LiteralPath $wvPath -Recurse -Force -ErrorAction Stop + $wvCleared = $true + } catch { } + } + } + if ($wvCleared) { substep "cleared stale WebView caches (ai.unsloth.studio); settings and data kept" } +} + # Back up User PATH under HKCU\Software\Unsloth before any modifications. try { $envKey = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $false) diff --git a/studio/setup.sh b/studio/setup.sh index 3c97f77065..749c585a26 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -475,6 +475,39 @@ rm -rf "$REPO_ROOT/unsloth_compiled_cache" rm -rf "$SCRIPT_DIR/backend/unsloth_compiled_cache" rm -rf "$SCRIPT_DIR/tmp/unsloth_compiled_cache" +# WebView caches keyed by the app bundle id hold copies of the previous +# frontend and can keep serving it after an update (old styles linger). +# Cache-only paths: LocalStorage, IndexedDB, cookies, settings, models, +# and the studio database are untouched. +_clear_webview_caches() { + _wvc_bid="ai.unsloth.studio" + _wvc_paths=() + case "$(uname -s 2>/dev/null)" in + Darwin) + _wvc_paths=( + "$HOME/Library/Caches/$_wvc_bid" + "$HOME/Library/WebKit/$_wvc_bid/WebsiteData/CacheStorage" + "$HOME/Library/WebKit/$_wvc_bid/WebsiteData/ServiceWorkers" + "$HOME/Library/WebKit/$_wvc_bid/WebsiteData/DiskCache" + ) + ;; + Linux) + _wvc_paths=("${XDG_CACHE_HOME:-$HOME/.cache}/$_wvc_bid") + ;; + *) return 0 ;; + esac + _wvc_cleared=false + for _wvc_p in "${_wvc_paths[@]}"; do + [ -e "$_wvc_p" ] || continue + rm -rf "$_wvc_p" 2>/dev/null && _wvc_cleared=true || true + done + if [ "$_wvc_cleared" = true ]; then + substep "cleared stale WebView caches ($_wvc_bid); settings and data kept" + fi + return 0 +} +_clear_webview_caches + # ── Detect Colab ── IS_COLAB=false keynames=$'\n'$(printenv | cut -d= -f1) diff --git a/tests/run_all.sh b/tests/run_all.sh index eaa726f73c..1b5609c475 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -14,6 +14,7 @@ sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh" sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh" sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh" sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh" +sh "$TESTS_DIR/sh/test_setup_webview_cache_clear.sh" sh "$TESTS_DIR/sh/test_torch_flavor.sh" sh "$TESTS_DIR/sh/test_redact_install_output.sh" sh "$TESTS_DIR/sh/test_install_uv_override_space.sh" diff --git a/tests/sh/test_setup_webview_cache_clear.sh b/tests/sh/test_setup_webview_cache_clear.sh new file mode 100755 index 0000000000..de2ab157d2 --- /dev/null +++ b/tests/sh/test_setup_webview_cache_clear.sh @@ -0,0 +1,92 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +# Unit tests for _clear_webview_caches() from studio/setup.sh. +# +# The WebView caches keyed by the app bundle id (ai.unsloth.studio) hold copies +# of the previous frontend, so an install/update must clear them or the app can +# keep rendering old styles. Clearing must be cache-only: LocalStorage, +# IndexedDB, app data, and unrelated apps' caches stay intact. +# +# Follows the extract-via-sed pattern of test_uninstall_shared_icon.sh; uname +# is overridden per test with a shell function to select the OS branch. +# shellcheck disable=SC2329 # uname stubs are invoked inside the extracted function +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +SETUP_SH="$SCRIPT_DIR/../../studio/setup.sh" +BID="ai.unsloth.studio" +PASS=0 +FAIL=0 + +_TMP_ROOT=$(mktemp -d) +trap 'rm -rf "$_TMP_ROOT"' EXIT + +assert_gone() { _l="$1"; if [ -e "$2" ]; then echo " FAIL: $_l (still present: $2)"; FAIL=$((FAIL+1)); else echo " PASS: $_l"; PASS=$((PASS+1)); fi; } +assert_present() { _l="$1"; if [ -e "$2" ]; then echo " PASS: $_l"; PASS=$((PASS+1)); else echo " FAIL: $_l (missing: $2)"; FAIL=$((FAIL+1)); fi; } + +# Extract just the function definition (top-level, closes at column 0). +FUNC_FILE=$(mktemp -p "$_TMP_ROOT") +sed -n '/^_clear_webview_caches() {/,/^}/p' "$SETUP_SH" > "$FUNC_FILE" +# shellcheck disable=SC1090 +. "$FUNC_FILE" +substep() { :; } # stub the setup.sh logger + +# ── 1. macOS: cache paths removed, user-facing storage kept ── +H=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$H/Library/Caches/$BID/WebKit/NetworkCache" \ + "$H/Library/WebKit/$BID/WebsiteData/CacheStorage" \ + "$H/Library/WebKit/$BID/WebsiteData/ServiceWorkers" \ + "$H/Library/WebKit/$BID/WebsiteData/DiskCache" \ + "$H/Library/WebKit/$BID/WebsiteData/LocalStorage" \ + "$H/Library/WebKit/$BID/WebsiteData/IndexedDB" \ + "$H/Library/Application Support/$BID" \ + "$H/Library/Caches/com.other.app" +uname() { echo Darwin; } +HOME="$H" _clear_webview_caches +assert_gone "macOS: Caches/$BID removed" "$H/Library/Caches/$BID" +assert_gone "macOS: WebsiteData/CacheStorage removed" "$H/Library/WebKit/$BID/WebsiteData/CacheStorage" +assert_gone "macOS: WebsiteData/ServiceWorkers removed" "$H/Library/WebKit/$BID/WebsiteData/ServiceWorkers" +assert_gone "macOS: WebsiteData/DiskCache removed" "$H/Library/WebKit/$BID/WebsiteData/DiskCache" +assert_present "macOS: LocalStorage kept" "$H/Library/WebKit/$BID/WebsiteData/LocalStorage" +assert_present "macOS: IndexedDB kept" "$H/Library/WebKit/$BID/WebsiteData/IndexedDB" +assert_present "macOS: Application Support kept" "$H/Library/Application Support/$BID" +assert_present "macOS: unrelated app cache kept" "$H/Library/Caches/com.other.app" + +# ── 2. Linux: cache dir removed, data/config kept ── +H=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$H/.cache/$BID" "$H/.local/share/$BID" "$H/.config/$BID" "$H/.cache/other.app" +uname() { echo Linux; } +HOME="$H" XDG_CACHE_HOME="" _clear_webview_caches +assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID" +assert_present "linux: ~/.local/share/$BID kept" "$H/.local/share/$BID" +assert_present "linux: ~/.config/$BID kept" "$H/.config/$BID" +assert_present "linux: unrelated app cache kept" "$H/.cache/other.app" + +# ── 3. Linux: XDG_CACHE_HOME override honored ── +H=$(mktemp -d -p "$_TMP_ROOT") +XDG=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$XDG/$BID" "$H/.cache/$BID" +HOME="$H" XDG_CACHE_HOME="$XDG" _clear_webview_caches +assert_gone "linux: XDG_CACHE_HOME/$BID removed" "$XDG/$BID" +assert_present "linux: ~/.cache/$BID kept under override" "$H/.cache/$BID" + +# ── 4. Nothing to clear is a clean no-op ── +H=$(mktemp -d -p "$_TMP_ROOT") +uname() { echo Darwin; } +if HOME="$H" _clear_webview_caches; then + echo " PASS: empty HOME -> no-op exit 0"; PASS=$((PASS+1)) +else + echo " FAIL: empty HOME -> nonzero exit"; FAIL=$((FAIL+1)) +fi + +# ── 5. Unknown OS is a no-op ── +H=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$H/Library/Caches/$BID" +uname() { echo SunOS; } +HOME="$H" _clear_webview_caches +assert_present "unknown OS: nothing removed" "$H/Library/Caches/$BID" + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" = 0 ] From 1eb3ac26e72a62a5f3cb375e1b9f0fc7ebea7f35 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:56:09 +0000 Subject: [PATCH 002/170] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/studio/playwright_ui_font_scale.py | 18 ++++++++---------- tests/studio/test_ui_font_scale_contract.py | 12 +++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/studio/playwright_ui_font_scale.py b/tests/studio/playwright_ui_font_scale.py index 7dbe36127c..89c7894929 100644 --- a/tests/studio/playwright_ui_font_scale.py +++ b/tests/studio/playwright_ui_font_scale.py @@ -38,7 +38,11 @@ def fail(m): raise AssertionError(f"[font-scale] FAIL: {m}") -def near(a, b, tol = 0.35): +def near( + a, + b, + tol = 0.35, +): return a is not None and b is not None and abs(a - b) <= tol @@ -86,9 +90,7 @@ def open_appearance(page): page.wait_for_timeout(700) if page.get_by_role("dialog").count() == 0: fail("settings dialog did not open") - page.get_by_role("dialog").get_by_role("button").filter( - has_text = "Appearance" - ).first.click() + page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click() page.wait_for_timeout(600) @@ -153,9 +155,7 @@ def main(): page.wait_for_timeout(400) step("overflowing select scrolls its Radix viewport") - page.get_by_role("dialog").get_by_role("button").filter( - has_text = "Voice" - ).first.click() + page.get_by_role("dialog").get_by_role("button").filter(has_text = "Voice").first.click() page.wait_for_timeout(600) page.set_viewport_size({"width": 1440, "height": 480}) page.locator("[aria-label='Dictation language']").click() @@ -194,9 +194,7 @@ def main(): page.wait_for_timeout(400) step("default restores exactly") - page.get_by_role("dialog").get_by_role("button").filter( - has_text = "Appearance" - ).first.click() + page.get_by_role("dialog").get_by_role("button").filter(has_text = "Appearance").first.click() page.wait_for_timeout(500) set_input(page, "UI font size", DEFAULT) final = measure(page) diff --git a/tests/studio/test_ui_font_scale_contract.py b/tests/studio/test_ui_font_scale_contract.py index 62bf80e82b..393c60dd6b 100644 --- a/tests/studio/test_ui_font_scale_contract.py +++ b/tests/studio/test_ui_font_scale_contract.py @@ -15,9 +15,7 @@ from pathlib import Path REPO = Path(__file__).resolve().parents[2] SRC = REPO / "studio/frontend/src" INDEX_CSS = (SRC / "index.css").read_text(encoding = "utf-8") -STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text( - encoding = "utf-8" -) +STORE = (SRC / "features/settings/stores/appearance-custom-store.ts").read_text(encoding = "utf-8") SELECT = (SRC / "components/ui/select.tsx").read_text(encoding = "utf-8") # Raw numeric fontSize props are only allowed where a scaled stylesheet rule @@ -70,9 +68,7 @@ def test_ui_token_families_exist(): def test_explicit_code_font_size_is_never_multiplied(): - match = re.search( - r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS - ) + match = re.search(r"html\[data-code-font-size\][^{]*\{([^}]*)\}", INDEX_CSS) assert match is not None body = match.group(1) assert "var(--custom-code-font-size)" in body @@ -83,7 +79,9 @@ def test_radix_select_viewport_owns_the_scroll_state(): viewport = SELECT[SELECT.index("SelectPrimitive.Viewport") :] assert "overflow-y-auto" in viewport.split("")[0] # The rounded surface itself must not scroll (WebKit squares its corners). - content_cls = re.search(r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT) + content_cls = re.search( + r"SelectPrimitive\.Content[\s\S]*?className=\{cn\(\s*\"([^\"]+)\"", SELECT + ) assert content_cls is not None assert "overflow-hidden" in content_cls.group(1) assert "overflow-y-auto" not in content_cls.group(1) From b0d6131567151f42f69d8eef390453190e98a4d8 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 23 Jul 2026 02:02:41 -0700 Subject: [PATCH 003/170] Security audit: refresh scan baselines for current dependency set (#7362) The pip scan-packages extras shard has been red on main because openai 2.47.0 changed the code inside five previously baselined findings, so their evidence hashes no longer matched the allowlist. The hf-stack shard was about to go red the same way: unsloth-zoo 2026.7.5 changed two baselined test files. All seven reopened findings were re-verified against the exact resolved archives before re-baselining: - openai/_base_client.py: while True in SyncPage.iter_pages, the pagination iterator. - openai/auth/_workload.py: Azure IMDS and GCP metadata token providers for the documented workload identity federation feature. - openai/resources/{beta/responses,realtime,responses}: while True in websocket __aiter__ event loops; the loop bodies gained reconnect handling in 2.47.0, which is what shifted the hashes. - unsloth-zoo tests/test_vision_collator_audio.py: asserts that an inline /tmp/a.wav path is passed through by the audio collator. - unsloth-zoo tests/test_gemma4_forced_float32_ple_dtype.py: compile()/exec() of the project's own generated Gemma4 PLE cast helper source in tests. No existing entries were removed. All three shards now exit 0 locally against the same requirement sets CI uses. --- scripts/scan_packages_baseline.json | 56 +++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/scripts/scan_packages_baseline.json b/scripts/scan_packages_baseline.json index 936f748a74..65b8d2b11c 100644 --- a/scripts/scan_packages_baseline.json +++ b/scripts/scan_packages_baseline.json @@ -1561,6 +1561,62 @@ "severity": "CRITICAL", "evidence": "L111: out = extract_audio_info(msgs({\"type\": \"audio\", key: \"/tmp/a.wav\"})) sha256:2efe23ffbe2b91b8403aec9b700736919b59e5ca770f8e1f5501651b44b7d398", "evidence_hash": "d416b79dd17b24214f3f7653ac01354507d7bf0fc464dee30a4a4b8998f063ba" + }, + { + "package": "openai", + "file": "openai/_base_client.py", + "check": "C2 polling/beaconing loop detected", + "severity": "CRITICAL", + "evidence": "L274: while True: sha256:90a38e5c1e26893c7c273354143612640e9a9c0f079d3e2b60612d79f24e80a6", + "evidence_hash": "1022e8e8649436ec64a98a9d9141d085452c49549fd2157b0278fc369a83ac66" + }, + { + "package": "openai", + "file": "openai/auth/_workload.py", + "check": "Accesses cloud metadata/IMDS AND makes network calls", + "severity": "CRITICAL", + "evidence": "IMDS: L97: url = \"http://169.254.169.254/metadata/identity/oauth2/token\" | L150: url = \"http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity\"\nNetwork: L78: http_client: httpx.Client | None = None, | L109: with httpx.Client() as client: | L134: http_client: httpx.Client | None = None, | L156: with httpx.Client() as client: | L251: exchange_client = DefaultHttpx2Client(follow_redirects=False) if self._use_httpx2 else httpx.Client()", + "evidence_hash": "9717e51cb961dc14c458955d91a1e48e3753997346ecea0106bded3a8d64bfe0" + }, + { + "package": "openai", + "file": "openai/resources/beta/responses/responses.py", + "check": "C2 polling/beaconing loop detected", + "severity": "CRITICAL", + "evidence": "L4000: while True: sha256:f8ab538118daba9ec06e27399dbdc90a4521c3390e6a47a6348a1f180a83effd", + "evidence_hash": "31481ea83c687acc27144d72d3832d4fb98dd1c79fb5e0ddd85080de95997b9f" + }, + { + "package": "openai", + "file": "openai/resources/realtime/realtime.py", + "check": "C2 polling/beaconing loop detected", + "severity": "CRITICAL", + "evidence": "L311: while True: sha256:5b63313072aae9ca28677e03426513ccf12221e4f4e0ea6c31efbe09790633b5", + "evidence_hash": "05e1af469d651b51673763a7c4cdf759af9472fb627b7b470adc28cc237bd650" + }, + { + "package": "openai", + "file": "openai/resources/responses/responses.py", + "check": "C2 polling/beaconing loop detected", + "severity": "CRITICAL", + "evidence": "L3951: while True: sha256:d68ef896bf0743ca430cfacb9a3353da1f3b9c51c3a21b6450a07a32b55aa2ac", + "evidence_hash": "160eecdd79b521bffbe8476f782b69a0724c35d1b19376a7600807165fd54f9f" + }, + { + "package": "unsloth-zoo", + "file": "tests/test_gemma4_forced_float32_ple_dtype.py", + "check": "Advanced obfuscation (marshal/compile/zlib) + exec/eval", + "severity": "HIGH", + "evidence": "Obfusc: L277: compile(rewritten + _GEMMA4_PLE_CAST_HELPER, \"\", \"exec\") | L440: compile(on, \"\", \"exec\") | L468: compile(generated, \"\", \"exec\")\nExec: L19: exec(_GEMMA4_PLE_CAST_HELPER, namespace)", + "evidence_hash": "a85e24d8e7c431563cbd83b70f91a3b971abde0f37083d68e70984147960cc70" + }, + { + "package": "unsloth-zoo", + "file": "tests/test_vision_collator_audio.py", + "check": "Writes to /tmp and executes (staged dropper)", + "severity": "CRITICAL", + "evidence": "L111: out = extract_audio_info(msgs({\"type\": \"audio\", key: \"/tmp/a.wav\"})) sha256:022f81dd21acfc6a35a058de96132834c218404a9e37b3d09a7768a8c8f6c728", + "evidence_hash": "2d1e75446af120d9133a42aa8af426a839d3434d9dc109cc1d6c1b22ca1ddb75" } ] } From 5aedfd0b46b5a5efe506f94deb55ae39dd202860 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 23 Jul 2026 02:13:51 -0700 Subject: [PATCH 004/170] Studio: always show the executed Python script in chat with a download option (#7240) The script the python tool runs was rendered inside a collapsible that closes when the run ends or the thread is reopened, so the code disappeared from the transcript and there was no way to save it. Render the script outside the collapsible so it stays visible, and add a Download button that saves it as script.py. Other tools and normal chat are unaffected. --- .../assistant-ui/tool-ui-python.tsx | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx index 1b3dd22000..e3f4a0aafe 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx @@ -8,7 +8,7 @@ import { getAuthToken } from "@/features/auth/session"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { useToolArgsStatus } from "@assistant-ui/react"; import { code as codePlugin } from "@streamdown/code"; -import { CodeIcon, CopyIcon } from "lucide-react"; +import { CodeIcon, CopyIcon, DownloadIcon } from "lucide-react"; import { Tick02Icon } from "@/lib/tick-icon"; import { HugeiconsIcon } from "@hugeicons/react"; import { Spinner } from "@/components/ui/spinner"; @@ -83,6 +83,41 @@ function CopyBtn({ text }: { text: string }) { ); } +/** Save the executed script as a .py file via a client-side Blob (no server file serving). */ +function DownloadBtn({ code, name = "script.py" }: { code: string; name?: string }) { + const download = useCallback(() => { + if (typeof document === "undefined") { + return; + } + try { + const blob = new Blob([code], { type: "text/x-python" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = name; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + // Revoke next tick, after the click consumes the URL. + setTimeout(() => URL.revokeObjectURL(url), 0); + } catch { + // Best-effort: never break the transcript over a download. + } + }, [code, name]); + + return ( + + ); +} + /** Syntax-highlighted code via Streamdown + shiki; inherits parent container. */ function HighlightedCode({ code: source, language }: { code: string; language: string }) { const markdown = useMemo( @@ -153,23 +188,32 @@ const PythonToolUIImpl: ToolCallMessagePartComponent = ({ const authToken = getAuthToken(); return ( - // Open when mounted mid-run so live output shows; collapsed from history. + // Run status and output collapse from history, but the script source is + // rendered outside ToolFallbackContent so it stays visible on reopen (#7165). + {code && ( +
+
+
+ + script + +
+ + +
+
+ +
+
+ )}
- {/* Code + copy */} - {code && ( -
- -
- )} - {code && } - {/* Output */} {isRunning ? ( <> From e143e1ce33b7c7cffcd25b0de85459698de2619e Mon Sep 17 00:00:00 2001 From: Long Yixing Date: Thu, 23 Jul 2026 17:14:20 +0800 Subject: [PATCH 005/170] feat(studio): Mac-aware training controls for MLX (optimizers, LoftQ, packing) (#7358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(studio): offer MLX-supported optimizers on Apple Silicon The training form's optimizer dropdown only listed CUDA/bitsandbytes optimizers (adamw_8bit, paged variants, torch fused). On Apple Silicon the MLX trainer supports a different set (adamw, adam, lion, muon, sgd, adafactor) and remaps every bitsandbytes/torch name to plain AdamW, so the dropdown misrepresented what actually runs. Offer the MLX optimizer list when the device is a Mac, and derive the displayed value so the control is never blank: the shared CUDA default and the other bitsandbytes/torch options render as AdamW (exactly how the MLX backend normalizes them), while any other value is shown as-is so an unrecognized or non-canonical imported optimizer is never mislabeled. Non-Mac behavior is unchanged. The run-summary optimizer label now resolves from both lists. * feat(studio): show an MLX-appropriate optimizer tooltip on Apple Silicon The optimizer tooltip described "8-bit variants" and recommended "Fused" for vision models, neither of which is offered when training runs on MLX. On Apple Silicon, show a tooltip that matches the MLX optimizer set and notes that Lion typically needs a lower learning rate than AdamW. Copy-only: no change to the selected optimizer or the learning rate, and the non-Mac tooltip is unchanged. The new string is added to the English locale; other locales fall back to English until translated, matching how new keys are handled elsewhere. * fix(studio): label Mac CUDA-alias optimizers as AdamW in the run summary On Apple Silicon the run-configuration summary looked up the stored optimizer name directly, so a run that kept a CUDA/bitsandbytes default such as adamw_8bit was labeled "AdamW 8-bit" even though the picker shows "AdamW" and the MLX backend runs plain AdamW. Mirror the training form's derivation so those aliases are labeled AdamW in the summary too. Display-only: no change to the stored or submitted optimizer, and non-Mac summaries are unchanged. * feat(studio): disable LoftQ and sequence packing on Apple Silicon Neither LoftQ nor sequence packing is supported on MLX — the backend rejects LoftQ and the trainer silently forces packing off — yet the training form still offered both on Apple Silicon. Disable the LoftQ LoRA-init option (greyed and unclickable, with an inline "Not supported on Apple Silicon" note) and the "Enable packing" checkbox (greyed, with a tooltip explaining why), matching how the unsupported "Enable streaming" control is presented. Clearing effects reset a stale loftq/packing value to its default on Mac so the disabled controls never submit it. Non-Mac behavior is unchanged. * [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> --- studio/frontend/src/config/training.ts | 13 +++ .../studio/sections/params-section.tsx | 79 +++++++++++++++++-- .../studio/sections/progress-section.tsx | 17 +++- studio/frontend/src/i18n/locales/en.ts | 2 + tests/studio/playwright_ui_font_scale.py | 18 ++--- tests/studio/test_ui_font_scale_contract.py | 12 ++- 6 files changed, 115 insertions(+), 26 deletions(-) diff --git a/studio/frontend/src/config/training.ts b/studio/frontend/src/config/training.ts index 873e9aa203..10f7e93e5e 100644 --- a/studio/frontend/src/config/training.ts +++ b/studio/frontend/src/config/training.ts @@ -92,6 +92,19 @@ export const OPTIMIZER_OPTIONS: ReadonlyArray<{ value: string; label: string }> { value: "adamw_torch_fused", label: "AdamW (PyTorch Fused)" }, ]; +// Optimizers the MLX trainer actually supports on Apple Silicon. Values must +// match SUPPORTED_MLX_OPTIMIZERS in unsloth-zoo's mlx/trainer.py; on MLX the +// bitsandbytes/torch names above have no meaning and are remapped to plain +// AdamW, so Studio offers this list instead when running on a Mac. +export const MLX_OPTIMIZER_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [ + { value: "adamw", label: "AdamW" }, + { value: "adam", label: "Adam" }, + { value: "lion", label: "Lion" }, + { value: "muon", label: "Muon" }, + { value: "sgd", label: "SGD" }, + { value: "adafactor", label: "Adafactor" }, +]; + export const LR_SCHEDULER_OPTIONS: ReadonlyArray<{ value: string; label: string }> = [ { value: "linear", label: "Linear" }, { value: "cosine", label: "Cosine" }, diff --git a/studio/frontend/src/features/studio/sections/params-section.tsx b/studio/frontend/src/features/studio/sections/params-section.tsx index 029fef4e19..3270eb4e3d 100644 --- a/studio/frontend/src/features/studio/sections/params-section.tsx +++ b/studio/frontend/src/features/studio/sections/params-section.tsx @@ -36,6 +36,7 @@ import { CONTEXT_LENGTHS, CPT_TARGET_MODULES, LR_SCHEDULER_OPTIONS, + MLX_OPTIMIZER_OPTIONS, OPTIMIZER_OPTIONS, TARGET_MODULES, } from "@/config/training"; @@ -204,6 +205,42 @@ export function ParamsSection(): ReactElement { setCtxInput(String(store.contextLength)); }, [store.contextLength]); + // On Apple Silicon the MLX trainer supports a different optimizer set than + // the CUDA/bitsandbytes list, so offer the MLX names there. + const isMac = platformDeviceType === "mac"; + const optimizerOptions = isMac ? MLX_OPTIMIZER_OPTIONS : OPTIMIZER_OPTIONS; + + // On Mac, the MLX backend normalizes every CUDA/bitsandbytes optimizer in + // OPTIMIZER_OPTIONS (including the shared default) to plain AdamW, so show + // AdamW for those to keep the control truthful and non-blank. Any other + // value -- an MLX optimizer the user picked, or an unrecognized/non-canonical + // imported one -- is shown as-is rather than mislabeled as AdamW, since the + // backend would run or reject it on its own terms. Non-Mac display unchanged. + const isCudaAliasOptimizer = OPTIMIZER_OPTIONS.some( + (o) => o.value === store.optimizerType, + ); + const selectedOptimizer = + isMac && isCudaAliasOptimizer ? "adamw" : store.optimizerType; + + // LoftQ is not supported on MLX (the backend rejects it), so clear a stale + // selection to lora on Apple Silicon -- whether persisted, applied from a + // model default, or imported -- so the backend never receives it. + const setLoraVariant = store.setLoraVariant; + useEffect(() => { + if (isMac && store.loraVariant === "loftq") { + setLoraVariant("lora"); + } + }, [isMac, store.loraVariant, setLoraVariant]); + + // Packing is not supported on MLX (the backend forces it off), so clear it on + // Apple Silicon -- the checkbox is disabled and the flag is never sent. + const setPacking = store.setPacking; + useEffect(() => { + if (isMac && store.packing) { + setPacking(false); + } + }, [isMac, store.packing, setPacking]); + const trySetContextLength = (input: string): number | null => { const n = Number(input); if (Number.isInteger(n) && n > 0) { @@ -706,8 +743,9 @@ export function ParamsSection(): ReactElement { ))} @@ -765,7 +805,11 @@ export function ParamsSection(): ReactElement { label={t("studio.params.optimizer")} tooltip={ <> - {t("studio.params.optimizerTooltip")}{" "} + {t( + isMac + ? "studio.params.optimizerTooltipMlx" + : "studio.params.optimizerTooltip", + )}{" "} (KV_CACHE_DTYPES); export const SPECULATIVE_TYPES = [ From d17567af3e5c3ac03a0379bcf69a8c405d87d079 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 <144546710+Souravrajvi0@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:53:24 +0530 Subject: [PATCH 034/170] fix(studio/colab): restore blank Colab iframe embed (#7344) (#7349) * fix(studio/colab): restore iframe embed via serve_kernel_port_as_iframe Colab's output sanitizer often strips custom