From 76a2f17470eec4255b0fc76da254d753db215190 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 24 Mar 2026 07:10:26 -0700 Subject: [PATCH 01/34] fix(studio): remove litellm dep (quarantined on PyPI) (#4553) litellm has been quarantined on PyPI due to a supply chain attack in version 1.82.8 (malicious credential-stealing .pth file). No versions are currently installable, which blocks `unsloth studio setup` at step 8/11 (data-designer deps). Remove litellm from the single-env data-designer requirements so setup completes. litellm can be re-added once PyPI lifts the quarantine. Ref: https://github.com/BerriAI/litellm/issues/24512 --- studio/backend/requirements/single-env/data-designer-deps.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/studio/backend/requirements/single-env/data-designer-deps.txt b/studio/backend/requirements/single-env/data-designer-deps.txt index e0b3d8b72a..9cd0db99e4 100644 --- a/studio/backend/requirements/single-env/data-designer-deps.txt +++ b/studio/backend/requirements/single-env/data-designer-deps.txt @@ -8,7 +8,6 @@ httpx-retries<1,>=0.4.2 json-repair<1,>=0.48.0 jsonpath-rust-bindings<2,>=1.0 jsonschema<5,>=4.0.0 -litellm<1.80.12,>=1.73.6 lxml<7,>=6.0.2 marko<3,>=2.1.2 networkx<4,>=3.0 From acc881452f11b7ec686c8a24d356fe014ef97551 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Tue, 24 Mar 2026 07:44:07 -0700 Subject: [PATCH 02/34] fix: pin unsloth>=2026.3.11 in install.sh and install.ps1 (#4556) Ensures both install scripts always pull a version that has the litellm removal fix. Without the pin, stale uv/pip caches could resolve the older 2026.3.10 which still had litellm in data-designer-deps.txt, causing setup to fail at step 8/11 while PyPI has litellm quarantined. --- install.ps1 | 2 +- install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/install.ps1 b/install.ps1 index d438e5ed2d..52386dd2fd 100644 --- a/install.ps1 +++ b/install.ps1 @@ -275,7 +275,7 @@ function Install-UnslothStudio { } Write-Host "==> Installing unsloth (this may take a few minutes)..." - uv pip install --python $VenvPython --upgrade-package unsloth unsloth + uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.3.11" if ($LASTEXITCODE -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $LASTEXITCODE)" -ForegroundColor Red return diff --git a/install.sh b/install.sh index 18db3469f4..3d8e08612c 100755 --- a/install.sh +++ b/install.sh @@ -235,7 +235,7 @@ fi # ── Install unsloth directly into the venv (no activation needed) ── echo "==> Installing unsloth (this may take a few minutes)..." -uv pip install --python "$VENV_NAME/bin/python" unsloth --torch-backend=auto +uv pip install --python "$VENV_NAME/bin/python" "unsloth>=2026.3.11" --torch-backend=auto # ── Run studio setup ── # Ensure the venv's Python is on PATH for setup.sh's Python discovery. From 085f9529b612f214a4e6057543b2bc2bdeddcc75 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Wed, 25 Mar 2026 03:39:27 +0100 Subject: [PATCH 03/34] Regroup chat settings sidebar into focused sections (#4551) * feat(chat): regroup settings sidebar into Model, Sampling, Tools, and Preferences sections Split the monolithic Settings collapsible into focused sections with icons. Model section shows context length and KV cache dtype for GGUF models, trust remote code for non GGUF. Tools section groups auto heal, max tool calls, and tool call timeout. Preferences section holds auto title toggle. * feat(chat): persist collapsible section open/closed state in localStorage Remember which sections the user expanded or collapsed across sidebar toggles, mobile sheet reopens, and browser sessions. * fix(chat): harden collapsible state persistence and restore defaultOpen - Validate localStorage values are booleans before using them, preventing corrupted entries like string "false" from being treated as truthy - Use Object.hasOwn() instead of `in` operator to avoid prototype chain matches on keys like "constructor" or "toString" - Restore defaultOpen={true} on Model and Preferences sections so they are expanded on first visit, matching the old Settings section behavior - Fix misleading Context Length description to reflect it is read-only - Downgrade console.error to console.warn for non-critical localStorage parse failures * fix(chat): remove redundant disabled styles on Context Length input The Input component already applies opacity-50 and cursor-not-allowed via its disabled: variants. Specifying them unconditionally in the className is redundant. --------- Co-authored-by: Daniel Han --- .../src/features/chat/chat-settings-sheet.tsx | 162 +++++++++++++----- 1 file changed, 116 insertions(+), 46 deletions(-) diff --git a/studio/frontend/src/features/chat/chat-settings-sheet.tsx b/studio/frontend/src/features/chat/chat-settings-sheet.tsx index 45c9d17888..081e3efc26 100644 --- a/studio/frontend/src/features/chat/chat-settings-sheet.tsx +++ b/studio/frontend/src/features/chat/chat-settings-sheet.tsx @@ -28,6 +28,8 @@ import { PencilEdit01Icon, Settings02Icon, SlidersHorizontalIcon, + UserSettings01Icon, + Wrench01Icon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { AnimatePresence, motion } from "motion/react"; @@ -164,6 +166,39 @@ function ParamSlider({ ); } +const COLLAPSIBLE_STATE_KEY = "unsloth_chat_collapsible_state"; + +function loadCollapsibleState(): Record { + if (!canUseStorage()) return {}; + try { + const raw = localStorage.getItem(COLLAPSIBLE_STATE_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) { + return {}; + } + return Object.fromEntries( + Object.entries(parsed).filter( + (entry): entry is [string, boolean] => typeof entry[1] === "boolean", + ), + ); + } catch (error) { + console.warn("Failed to load collapsible state from localStorage:", error); + return {}; + } +} + +function saveCollapsibleOpen(label: string, open: boolean) { + if (!canUseStorage()) return; + try { + const state = loadCollapsibleState(); + state[label] = open; + localStorage.setItem(COLLAPSIBLE_STATE_KEY, JSON.stringify(state)); + } catch { + // ignore + } +} + function CollapsibleSection({ icon, label, @@ -175,13 +210,20 @@ function CollapsibleSection({ children?: ReactNode; defaultOpen?: boolean; }) { - const [open, setOpen] = useState(defaultOpen); + const [open, setOpen] = useState(() => { + const saved = loadCollapsibleState(); + return Object.hasOwn(saved, label) ? saved[label] : defaultOpen; + }); return (
+ +
+ {isGguf && ( + <> +
+
+
Context Length
+
+ Reported by the loaded GGUF model. +
+
+ +
+
+
+
KV Cache Dtype
+
+ Quantize KV cache to reduce VRAM. Reload to apply. +
+
+ +
+ + )} + {!isGguf && ( +
+
+
Trust remote code
+
+ Allow models with custom code (e.g. Nemotron). Only enable for repos you trust. +
+
+ +
+ )} +
+
+ - + +
+ + + +
+
+ +
@@ -519,49 +632,6 @@ export function ChatSettingsPanel({ onCheckedChange={onAutoTitleChange} />
-
-
-
Trust remote code
-
- Allow models with custom code (e.g. Nemotron). Only enable for repos you trust. -
-
- -
- {isGguf && ( -
-
-
KV Cache Dtype
-
- Quantize KV cache to reduce VRAM. Reload to apply. -
-
- -
- )} - - -
From 8c94b461fb8c537da89169e0d69871a114138efd Mon Sep 17 00:00:00 2001 From: TR-3B <144127816+MagellaX@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:07:45 +0530 Subject: [PATCH 04/34] Add GRPO resume vLLM cleanup guard (#4411) * Add GRPO resume vLLM cleanup guard * Guard GRPO resume sleep on vLLM sleep mode * Harden GRPO resume vLLM cleanup guard - Wrap llm.sleep(1) in try/except so a failed sleep does not block training resume (best-effort cleanup) - Also check kwargs["model_path"] which transformers.Trainer.train() still accepts and normalizes to resume_from_checkpoint internally --------- Co-authored-by: Daniel Han --- unsloth/models/rl.py | 65 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index a05186eee5..52fd7f7ecc 100755 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -93,6 +93,58 @@ def vLLMSamplingParams(**kwargs): return sampling_params +def _maybe_prepare_vllm_for_resume(trainer): + if not torch.cuda.is_available(): + return + + llm = getattr(trainer, "llm", None) + if llm is None: + llm = getattr(getattr(trainer, "model", None), "vllm_engine", None) + if llm is None: + return + + model_config = getattr( + getattr(getattr(llm, "llm_engine", None), "vllm_config", None), + "model_config", + None, + ) + if not getattr(model_config, "enable_sleep_mode", False): + return + + try: + llm.sleep(1) + except Exception: + pass + + import gc + + for _ in range(3): + gc.collect() + torch.cuda.empty_cache() + + +def _patch_resume_from_checkpoint_memory(trainer_class): + original_train = getattr(trainer_class, "train", None) + if original_train is None: + return + if getattr(original_train, "_unsloth_resume_guard", False): + return + + def _unsloth_train_with_resume_guard(self, *args, **kwargs): + resume_from_checkpoint = kwargs.get("resume_from_checkpoint", None) + if resume_from_checkpoint is None: + resume_from_checkpoint = kwargs.get("model_path", None) + if resume_from_checkpoint is None and len(args) != 0: + resume_from_checkpoint = args[0] + + if resume_from_checkpoint: + _maybe_prepare_vllm_for_resume(self) + return original_train(self, *args, **kwargs) + + _unsloth_train_with_resume_guard._unsloth_resume_guard = True + trainer_class.train = _unsloth_train_with_resume_guard + + def PatchRL(FastLanguageModel): try: from trl.models.utils import unwrap_model_for_generation @@ -686,8 +738,8 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): else: continue call_args.append(f"{k} = {k}") - arguments = f"\n{' '*8}" + f",\n{' '*8}".join(arguments) - call_args = f"\n{' '*12}" + f",\n{' '*12}".join(call_args) + arguments = f"\n{' ' * 8}" + f",\n{' ' * 8}".join(arguments) + call_args = f"\n{' ' * 12}" + f",\n{' ' * 12}".join(call_args) processed.append( ( arguments, @@ -701,7 +753,7 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): # Add tokenizer if not seen if "tokenizer" not in parameters and "processing_class" in parameters: - arguments += f",\n{' '*8}tokenizer = None" + arguments += f",\n{' ' * 8}tokenizer = None" call_args = call_args.replace( "processing_class = processing_class", "processing_class = tokenizer if tokenizer is not None else processing_class", @@ -1490,6 +1542,9 @@ def _patch_trl_rl_trainers(trainer_file = "grpo_trainer"): imports, overwrite = False, ) + patched_trainer = getattr(created_module, f"Unsloth{RLTrainer_name}") + if trainer_file == "grpo_trainer": + _patch_resume_from_checkpoint_memory(patched_trainer) # Patch Trainer exec( @@ -1706,8 +1761,8 @@ def patch_functions(RLTrainer, trainer_file, RLTrainer_name, all_imports, import sampling_params = re.sub(r"[\,][\s]{0,}\,", ",", sampling_params) new_vllm_part = ( - f"\n{' '*8}if {args}.use_vllm:\n{sampling_params}" - f"\n{' '*8}else:\n" + f"\n{' ' * 8}if {args}.use_vllm:\n{sampling_params}" + f"\n{' ' * 8}else:\n" ) if trl_version >= Version("0.18.0"): From 9b989ee8986c51a98ba1a145159473629991ab98 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Tue, 24 Mar 2026 22:04:09 -0700 Subject: [PATCH 05/34] fix: prevent UnicodeEncodeError on Windows CP1252 consoles in studio setup (#4563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: prevent UnicodeEncodeError on Windows CP1252 consoles in studio setup On Windows, `unsloth studio setup` crashes with a UnicodeEncodeError when install_python_stack.py tries to print Unicode status glyphs (✅, ❌, ⚠️) to a console that uses a legacy code page like CP1252. Add a _safe_print() helper that catches UnicodeEncodeError and gracefully degrades emoji to ASCII equivalents ([OK], [FAIL], [!]). Replace all print() calls that emit Unicode glyphs with _safe_print(). Fixes #4509 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Replace Unicode dashes with ASCII in install_python_stack.py Box-drawing (U+2500) and em dash (U+2014) chars in section dividers and comments are themselves not representable on CP1252 -- replace with plain ASCII dashes for consistency with the fix. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- studio/install_python_stack.py | 64 ++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index a141c64425..9b2678f478 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -22,20 +22,20 @@ from pathlib import Path IS_WINDOWS = sys.platform == "win32" -# ── Verbosity control ────────────────────────────────────────────────────────── +# -- Verbosity control ---------------------------------------------------------- # By default the installer shows a minimal progress bar (one line, in-place). # Set UNSLOTH_VERBOSE=1 in the environment to restore full per-step output: # Linux/Mac: UNSLOTH_VERBOSE=1 ./studio/setup.sh # Windows: $env:UNSLOTH_VERBOSE="1" ; .\studio\setup.ps1 VERBOSE: bool = os.environ.get("UNSLOTH_VERBOSE", "0") == "1" -# Progress bar state — updated by _progress() as each install step runs. +# Progress bar state -- updated by _progress() as each install step runs. # _TOTAL counts: pip-upgrade + 7 shared steps + triton (non-Windows) + local-plugin + finalize # Update _TOTAL here if you add or remove install steps in install_python_stack(). _STEP: int = 0 _TOTAL: int = 0 # set at runtime in install_python_stack() based on platform -# ── Paths ────────────────────────────────────────────────────────────── +# -- Paths -------------------------------------------------------------- SCRIPT_DIR = Path(__file__).resolve().parent REQ_ROOT = SCRIPT_DIR / "backend" / "requirements" SINGLE_ENV = REQ_ROOT / "single-env" @@ -44,7 +44,39 @@ LOCAL_DD_UNSTRUCTURED_PLUGIN = ( SCRIPT_DIR / "backend" / "plugins" / "data-designer-unstructured-seed" ) -# ── Color support ────────────────────────────────────────────────────── +# -- Unicode-safe printing --------------------------------------------- +# On Windows the default console encoding can be a legacy code page +# (e.g. CP1252) that cannot represent Unicode glyphs such as ✅ or ❌. +# _safe_print() gracefully degrades to ASCII equivalents so the +# installer never crashes just because of a status glyph. + +_UNICODE_TO_ASCII: dict[str, str] = { + "\u2705": "[OK]", # ✅ + "\u274c": "[FAIL]", # ❌ + "\u26a0\ufe0f": "[!]", # ⚠️ (warning + variation selector) + "\u26a0": "[!]", # ⚠ (warning without variation selector) +} + + +def _safe_print(*args: object, **kwargs: object) -> None: + """Drop-in print() replacement that survives non-UTF-8 consoles.""" + try: + print(*args, **kwargs) + except UnicodeEncodeError: + # Stringify, then swap emoji for ASCII equivalents + text = " ".join(str(a) for a in args) + for uni, ascii_alt in _UNICODE_TO_ASCII.items(): + text = text.replace(uni, ascii_alt) + # Final fallback: replace any remaining unencodable chars + print( + text.encode(sys.stdout.encoding or "ascii", errors = "replace").decode( + sys.stdout.encoding or "ascii", errors = "replace" + ), + **kwargs, + ) + + +# -- Color support ------------------------------------------------------ def _enable_colors() -> bool: @@ -72,7 +104,7 @@ def _enable_colors() -> bool: return True # Unix terminals support ANSI by default -# Colors disabled — Colab and most CI runners render ANSI fine, but plain output +# Colors disabled -- Colab and most CI runners render ANSI fine, but plain output # is cleaner in the notebook cell. Re-enable by setting _HAS_COLOR = _enable_colors() _HAS_COLOR = False @@ -92,7 +124,7 @@ def _red(msg: str) -> str: def _progress(label: str) -> None: """Print an in-place progress bar for the current install step. - Uses only stdlib (sys.stdout) — no extra packages required. + Uses only stdlib (sys.stdout) -- no extra packages required. In VERBOSE mode this is a no-op; per-step labels are printed by run() instead. """ global _STEP @@ -119,7 +151,7 @@ def run( stderr = subprocess.STDOUT if quiet else None, ) if result.returncode != 0: - print(_red(f"❌ {label} failed (exit code {result.returncode}):")) + _safe_print(_red(f"❌ {label} failed (exit code {result.returncode}):")) if result.stdout: print(result.stdout.decode(errors = "replace")) sys.exit(result.returncode) @@ -129,7 +161,7 @@ def run( # Packages to skip on Windows (require special build steps) WINDOWS_SKIP_PACKAGES = {"open_spiel", "triton_kernels"} -# ── uv bootstrap ────────────────────────────────────────────────────── +# -- uv bootstrap ------------------------------------------------------ USE_UV = False # Set by _bootstrap_uv() at the start of install_python_stack() UV_NEEDS_SYSTEM = False # Set by _bootstrap_uv() via probe @@ -267,7 +299,9 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: text = True, ) if result.returncode != 0: - print(_red(f" ⚠️ Could not find package {package_name}, skipping patch")) + _safe_print( + _red(f" ⚠️ Could not find package {package_name}, skipping patch") + ) return location = None @@ -277,7 +311,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: break if not location: - print(_red(f" ⚠️ Could not determine location of {package_name}")) + _safe_print(_red(f" ⚠️ Could not determine location of {package_name}")) return dest = Path(location) / relative_path @@ -285,7 +319,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: download_file(url, dest) -# ── Main install sequence ───────────────────────────────────────────── +# -- Main install sequence --------------------------------------------- def install_python_stack() -> int: @@ -316,7 +350,7 @@ def install_python_stack() -> int: req = REQ_ROOT / "extras.txt", ) - # 3b. Extra dependencies (no-deps) — audio model support etc. + # 3b. Extra dependencies (no-deps) -- audio model support etc. _progress("extra codecs") pip_install( "Installing extras (no-deps)", @@ -325,7 +359,7 @@ def install_python_stack() -> int: req = REQ_ROOT / "extras-no-deps.txt", ) - # 4. Overrides (torchao, transformers) — force-reinstall + # 4. Overrides (torchao, transformers) -- force-reinstall _progress("dependency overrides") pip_install( "Installing dependency overrides", @@ -393,7 +427,7 @@ def install_python_stack() -> int: # 11. Local Data Designer seed plugin if not LOCAL_DD_UNSTRUCTURED_PLUGIN.is_dir(): - print( + _safe_print( _red( f"❌ Missing local plugin directory: {LOCAL_DD_UNSTRUCTURED_PLUGIN}", ), @@ -422,7 +456,7 @@ def install_python_stack() -> int: stderr = subprocess.DEVNULL, ) - print(_green("✅ Python dependencies installed")) + _safe_print(_green("✅ Python dependencies installed")) return 0 From 557743f027a5449cb0c055f40f9d94f136de4b8e Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Wed, 25 Mar 2026 06:41:02 +0000 Subject: [PATCH 06/34] studio: windows desktop shortcut launcher (#4558) * feat(windows): add Studio desktop/Start shortcuts with health-check launcher * chore(windows): bundle sloth.ico and set shortcut icons when valid * chore(windows):add images/sloth.ico * fix(windows): guard PSScriptRoot for Studio shortcut icon in iex installs * fix(install): high-DPI sloth.ico and relocate to studio/frontend/publi * chore(studio): update sloth.ico for clearer desktop and shell icons * chore(studio): use unsloth.ico for Studio shortcut icon * feat(windows): improve Studio shortcut launcher (fast health + browser UX) * fix(windows): stable unsloth.ico URL and Unicode-safe Studio launcher scripts * fix(windows): escape $ in exe path and write launcher UTF-8 with BOM * fix(windows): skip shortcuts when Desktop or APPDATA paths are missing * fix(install): log shortcut/icon/port failures and warn early on missing paths * fix(install): guard missing LOCALAPPDATA before shortcut paths * fix(install): harden New-StudioShortcuts and improve success messaging * fix(install): include port 8908 in studio health check * fix(install): fix launch-studio.ps1 quoting * Fix launcher edge cases and normalize indentation in install.ps1 - Handle silent timeout: show a message when Studio is still starting but did not become healthy within the timeout, instead of exiting with no feedback - Add -NoProfile to the visible PowerShell terminal launch so the user profile cannot hang or error before Studio runs - Add a named mutex (Local\UnslothStudioLauncher) to prevent double-click from spawning duplicate terminals; second instance polls for health and opens the browser when ready - Normalize indentation inside New-StudioShortcuts outer try block from mixed 8/12-space to consistent 12-space * Simplify Get-CandidatePorts port dedup with Sort-Object -Unique Replace the foreach/-notcontains loop with a single pipeline: $ports = (@($basePort) + $listening) | Sort-Object -Unique * Harden health probe and handle abandoned mutex in launcher - Test-StudioHealth now checks resp.service == 'Unsloth UI Backend' to avoid fingerprinting collisions with other local services on the same port range. - Wrap the mutex WaitOne(0) call in a try/catch for AbandonedMutexException so the launcher recovers gracefully when a previous instance was killed while holding the mutex. --------- Co-authored-by: Daniel Han --- install.ps1 | 271 +++++++++++++++++++++++++++++ studio/frontend/public/unsloth.ico | Bin 0 -> 160575 bytes 2 files changed, 271 insertions(+) create mode 100644 studio/frontend/public/unsloth.ico diff --git a/install.ps1 b/install.ps1 index 52386dd2fd..1613ec6258 100644 --- a/install.ps1 +++ b/install.ps1 @@ -31,6 +31,275 @@ function Install-UnslothStudio { $env:Path = $unique -join ";" } + function New-StudioShortcuts { + param( + [Parameter(Mandatory = $true)][string]$UnslothExePath + ) + + if (-not (Test-Path $UnslothExePath)) { + Write-Host "[WARN] Cannot create shortcuts: unsloth.exe not found at $UnslothExePath" -ForegroundColor Yellow + return + } + try { + # Persist an absolute path in launcher scripts so shortcut working + # directory changes do not break process startup. + $UnslothExePath = (Resolve-Path $UnslothExePath).Path + # Escape for single-quoted embedding in generated launcher script. + # This prevents runtime variable expansion for paths containing '$'. + $SingleQuotedExePath = $UnslothExePath -replace "'", "''" + + $localAppDataDir = $env:LOCALAPPDATA + if (-not $localAppDataDir -or [string]::IsNullOrWhiteSpace($localAppDataDir)) { + Write-Host "[WARN] LOCALAPPDATA path unavailable; skipped shortcut creation" -ForegroundColor Yellow + return + } + $appDir = Join-Path $localAppDataDir "Unsloth Studio" + $launcherPs1 = Join-Path $appDir "launch-studio.ps1" + $launcherVbs = Join-Path $appDir "launch-studio.vbs" + $desktopDir = [Environment]::GetFolderPath("Desktop") + $desktopLink = if ($desktopDir -and $desktopDir.Trim()) { + Join-Path $desktopDir "Unsloth Studio.lnk" + } else { + $null + } + $startMenuDir = if ($env:APPDATA -and $env:APPDATA.Trim()) { + Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs" + } else { + $null + } + $startMenuLink = if ($startMenuDir -and $startMenuDir.Trim()) { + Join-Path $startMenuDir "Unsloth Studio.lnk" + } else { + $null + } + if (-not $desktopLink) { + Write-Host "[WARN] Desktop path unavailable; skipped desktop shortcut creation" -ForegroundColor Yellow + } + if (-not $startMenuLink) { + Write-Host "[WARN] APPDATA/Start Menu path unavailable; skipped Start menu shortcut creation" -ForegroundColor Yellow + } + $iconPath = Join-Path $appDir "unsloth.ico" + $bundledIcon = $null + if ($PSScriptRoot -and $PSScriptRoot.Trim()) { + $bundledIcon = Join-Path $PSScriptRoot "studio\frontend\public\unsloth.ico" + } + $iconUrl = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth.ico" + + if (-not (Test-Path $appDir)) { + New-Item -ItemType Directory -Path $appDir -Force | Out-Null + } + + $launcherContent = @" +`$ErrorActionPreference = 'Stop' +`$basePort = 8888 +`$maxPortOffset = 20 +`$timeoutSec = 60 +`$pollIntervalMs = 1000 + +function Test-StudioHealth { + param([Parameter(Mandatory = `$true)][int]`$Port) + try { + `$url = "http://127.0.0.1:`$Port/api/health" + `$resp = Invoke-RestMethod -Uri `$url -TimeoutSec 1 -Method Get + return (`$resp -and `$resp.status -eq 'healthy' -and `$resp.service -eq 'Unsloth UI Backend') + } catch { + return `$false + } +} + +function Get-CandidatePorts { + # Fast path: only probe base port + currently listening ports in range. + `$ports = @(`$basePort) + try { + `$maxPort = `$basePort + `$maxPortOffset + `$listening = Get-NetTCPConnection -State Listen -ErrorAction Stop | + Where-Object { `$_.LocalPort -ge `$basePort -and `$_.LocalPort -le `$maxPort } | + Select-Object -ExpandProperty LocalPort + `$ports = (@(`$basePort) + `$listening) | Sort-Object -Unique + } catch { + Write-Host "[DEBUG] Get-NetTCPConnection failed: `$(`$_.Exception.Message). Falling back to full port scan." -ForegroundColor DarkGray + # Fallback when Get-NetTCPConnection is unavailable/restricted. + for (`$offset = 1; `$offset -le `$maxPortOffset; `$offset++) { + `$ports += (`$basePort + `$offset) + } + } + return `$ports +} + +function Find-HealthyStudioPort { + foreach (`$candidate in (Get-CandidatePorts)) { + if (Test-StudioHealth -Port `$candidate) { + return `$candidate + } + } + return `$null +} + +# If Studio is already healthy on any expected port, just open it and exit. +`$existingPort = Find-HealthyStudioPort +if (`$existingPort) { + Start-Process "http://localhost:`$existingPort" + exit 0 +} + +`$launchMutex = [System.Threading.Mutex]::new(`$false, 'Local\UnslothStudioLauncher') +`$haveMutex = `$false +try { + try { + `$haveMutex = `$launchMutex.WaitOne(0) + } catch [System.Threading.AbandonedMutexException] { + `$haveMutex = `$true + } + if (-not `$haveMutex) { + # Another launcher is already running; wait for it to bring Studio up + `$deadline = (Get-Date).AddSeconds(`$timeoutSec) + while ((Get-Date) -lt `$deadline) { + `$port = Find-HealthyStudioPort + if (`$port) { Start-Process "http://localhost:`$port"; exit 0 } + Start-Sleep -Milliseconds `$pollIntervalMs + } + exit 0 + } + + `$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' + `$studioExe = '$SingleQuotedExePath' + `$studioCommand = '& "' + `$studioExe + '" studio -H 0.0.0.0 -p ' + `$basePort + `$launchArgs = @( + '-NoExit', + '-NoProfile', + '-ExecutionPolicy', + 'Bypass', + '-Command', + `$studioCommand + ) + + try { + `$proc = Start-Process -FilePath `$powershellExe -ArgumentList `$launchArgs -WorkingDirectory `$env:USERPROFILE -PassThru + } catch { + `$msg = "Could not launch Unsloth Studio terminal.`n`nError: `$(`$_.Exception.Message)" + try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop + [System.Windows.Forms.MessageBox]::Show(`$msg, 'Unsloth Studio') | Out-Null + } catch {} + exit 1 + } + + `$browserOpened = `$false + `$deadline = (Get-Date).AddSeconds(`$timeoutSec) + while ((Get-Date) -lt `$deadline) { + `$healthyPort = Find-HealthyStudioPort + if (`$healthyPort) { + Start-Process "http://localhost:`$healthyPort" + `$browserOpened = `$true + break + } + if (`$proc.HasExited) { break } + Start-Sleep -Milliseconds `$pollIntervalMs + } + if (-not `$browserOpened) { + if (`$proc.HasExited) { + `$msg = "Unsloth Studio exited before becoming healthy. Check terminal output for errors." + } else { + `$msg = "Unsloth Studio is still starting but did not become healthy within `$timeoutSec seconds. Check the terminal window for the selected port and open it manually." + } + try { + Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop + [System.Windows.Forms.MessageBox]::Show(`$msg, 'Unsloth Studio') | Out-Null + } catch {} + } +} finally { + if (`$haveMutex) { `$launchMutex.ReleaseMutex() | Out-Null } + `$launchMutex.Dispose() +} +exit 0 +"@ + + # Write UTF-8 with BOM for reliable decoding by Windows PowerShell 5.1, + # even when install.ps1 is executed from PowerShell 7. + $utf8Bom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($launcherPs1, $launcherContent, $utf8Bom) + $vbsContent = @" +Set shell = CreateObject("WScript.Shell") +cmd = "powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""$launcherPs1""" +shell.Run cmd, 0, False +"@ + # WSH handles UTF-16LE reliably for .vbs files with non-ASCII paths. + Set-Content -Path $launcherVbs -Value $vbsContent -Encoding Unicode -Force + + # Prefer bundled icon from local clone/dev installs. + # If not available, best-effort download from raw GitHub. + # We only attach the icon if the resulting file has a valid ICO header. + $hasValidIcon = $false + if ($bundledIcon -and (Test-Path $bundledIcon)) { + try { + Copy-Item -Path $bundledIcon -Destination $iconPath -Force + } catch { + Write-Host "[DEBUG] Error copying bundled icon: $($_.Exception.Message)" -ForegroundColor DarkGray + } + } elseif (-not (Test-Path $iconPath)) { + try { + Invoke-WebRequest -Uri $iconUrl -OutFile $iconPath -UseBasicParsing + } catch { + Write-Host "[DEBUG] Error downloading icon: $($_.Exception.Message)" -ForegroundColor DarkGray + } + } + + if (Test-Path $iconPath) { + try { + $bytes = [System.IO.File]::ReadAllBytes($iconPath) + if ( + $bytes.Length -ge 4 -and + $bytes[0] -eq 0 -and + $bytes[1] -eq 0 -and + $bytes[2] -eq 1 -and + $bytes[3] -eq 0 + ) { + $hasValidIcon = $true + } else { + Remove-Item $iconPath -Force -ErrorAction SilentlyContinue + } + } catch { + Write-Host "[DEBUG] Error validating or removing icon: $($_.Exception.Message)" -ForegroundColor DarkGray + Remove-Item $iconPath -Force -ErrorAction SilentlyContinue + } + } + + $wscriptExe = Join-Path $env:SystemRoot "System32\wscript.exe" + $shortcutArgs = "//B //Nologo `"$launcherVbs`"" + + try { + $wshell = New-Object -ComObject WScript.Shell + $createdShortcutCount = 0 + foreach ($linkPath in @($desktopLink, $startMenuLink)) { + if (-not $linkPath -or [string]::IsNullOrWhiteSpace($linkPath)) { continue } + try { + $shortcut = $wshell.CreateShortcut($linkPath) + $shortcut.TargetPath = $wscriptExe + $shortcut.Arguments = $shortcutArgs + $shortcut.WorkingDirectory = $appDir + $shortcut.Description = "Launch Unsloth Studio" + if ($hasValidIcon) { + $shortcut.IconLocation = "$iconPath,0" + } + $shortcut.Save() + $createdShortcutCount++ + } catch { + Write-Host "[WARN] Could not create shortcut at ${linkPath}: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + if ($createdShortcutCount -gt 0) { + Write-Host "[OK] Created Unsloth Studio shortcut(s): $createdShortcutCount" -ForegroundColor Green + } else { + Write-Host "[WARN] No Unsloth Studio shortcuts were created" -ForegroundColor Yellow + } + } catch { + Write-Host "[WARN] Shortcut creation unavailable: $($_.Exception.Message)" -ForegroundColor Yellow + } + } catch { + Write-Host "[WARN] Shortcut setup failed; skipping shortcuts: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + # ── Check winget ── if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { Write-Host "Error: winget is not available." -ForegroundColor Red @@ -299,6 +568,8 @@ function Install-UnslothStudio { return } + New-StudioShortcuts -UnslothExePath $UnslothExe + Write-Host "" Write-Host "=========================================" Write-Host " Unsloth Studio installed!" diff --git a/studio/frontend/public/unsloth.ico b/studio/frontend/public/unsloth.ico new file mode 100644 index 0000000000000000000000000000000000000000..974a6ed05918cc3ac72789a187535f4707869345 GIT binary patch literal 160575 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%0A7LJl=K^6zL{4(#z<~@vNs;ZzmeGiBb6HDn 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 Date: Tue, 24 Mar 2026 23:48:49 -0700 Subject: [PATCH 07/34] Remove duplicate frontend assets from wheel to reduce package size (#4567) The wheel currently ships frontend/public/, frontend/src/, and frontend/*.lock alongside frontend/dist/. These are build-time inputs that Vite already copies into dist/ during the build step: - public/ is copied verbatim into dist/ by vite build (28.6 MB duplicate) - src/ is TSX source compiled into dist/assets/*.js (2.1 MB, not used at runtime) - *.lock files are package manager lockfiles (0.9 MB, not used at runtime) The backend only serves from frontend/dist/ (see main.py setup_frontend and run.py frontend_path). Nothing references public/ or src/ at runtime. This drops the wheel from ~62.7 MB to ~31 MB. --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ba75cec594..0c7dd0b962 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,12 +46,9 @@ studio = [ "*.ps1", "*.bat", "frontend/dist/**/*", - "frontend/public/**/*", - "frontend/src/**/*", "frontend/*.json", "frontend/*.ts", "frontend/*.js", - "frontend/*.lock", "frontend/*.html", "frontend/*.yaml", "frontend/.git*", From 208862218d60d4eb5010abe1838ed7fce4210f72 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Wed, 25 Mar 2026 08:58:55 +0100 Subject: [PATCH 08/34] feat(studio): training history persistence and past runs viewer (#4501) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(db): add SQLite storage layer for training history * feat(api): add training history endpoints and response models * feat(training): integrate DB persistence into training event loop * feat(ui): add training history views and card grid * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): address review issues in training history persistence - Strip hf_token/wandb_token from config before SQLite storage - Add UUID suffix to job_id for collision resistance - Use isfinite() for 0.0 metric handling throughout - Respect _should_stop in error event finalization - Run schema DDL once per process, not per connection - Close connection on schema init failure - Guard cleanup_orphaned_runs at startup - Cap _metric_buffer at 500 entries - Make FLUSH_THRESHOLD a class constant - Map 'running' to 'training' phase in historical view - Derive LR/GradNorm from history arrays in historical view - Fix nested button with div[role=button] in history cards - Guard String(value) against null/undefined in config popover - Clear selectedHistoryRunId on auto tab switch * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): address round-2 review findings across training backend and frontend Backend (training.py): - Move state mutation after proc.start() so a failed spawn does not wedge the backend with is_training=True - Create DB run row eagerly after proc.start() so runs appear in history during model loading, not after first metric event - Rewrite _flush_metrics_to_db() with snapshot-before-insert pattern to preserve metrics arriving during the write and retain buffer on failure - Guard eval_loss with float() coercion and math.isfinite(), matching the existing grad_norm guard - Increase pump thread join timeout from 3s to 8s to cover SQLite's default 5s lock timeout Frontend (studio-page.tsx): - Fix history navigation: check isTrainingRunning instead of showTrainingView in onSelectRun so completed runs are not misrouted - Replace activeTab state + auto-switch useEffect with derived tab to eliminate react-hooks/set-state-in-effect lint violation Frontend (historical-training-view.tsx): - Add explicit "running" branch to message ternary so running runs no longer fall through to "Training errored" - Derive loading from detail/error state and move cleanup to effect return to eliminate react-hooks/set-state-in-effect lint violation Frontend (progress-section.tsx): - Derive stopRequested from isTrainingRunning && stopRequestedLocal to eliminate react-hooks/set-state-in-effect lint violation and remove unused useEffect import * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix(studio): resolve 3 remaining bugs from round-2 review 1. Stuck on Current Run tab [12/20]: Only force "current-run" tab when isTrainingRunning is true, not when stale completed-run data exists. After training ends, users can freely navigate to Configure. 2. Incomplete metric sanitization [7/20]: Apply float() coercion and isfinite() guards to loss and learning_rate, matching the existing pattern used by grad_norm and eval_loss. Prevents TypeError from string values and NaN leaks into history arrays. 3. Stop button state leak across runs [10/20]: Add key={runtime.jobId} to ProgressSection so React remounts it when a new run starts, resetting stopRequestedLocal state. * fix(studio): deduplicate loss/lr sanitization in training event handler Reuse _safe_loss/_safe_lr from the progress update block instead of re-sanitizing the same raw event values for metric history. * fix(studio): restore loss > 0 guard to prevent eval steps injecting 0.0 into metric histories Round-2/3 fixes relaxed the history append guard from `loss > 0` to `loss is not None`, which let eval-only log events (where loss defaults to 0.0) append fake zeros into loss_history and lr_history. Restore the `loss > 0` check to match the worker's own has_train_loss gate. The float() coercion and isfinite() sanitization from round-3 remain intact. * fix(studio): resolve training history bugs — nullable loss/lr, tab nav, sparkline * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [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 --- studio/backend/core/training/trainer.py | 8 +- studio/backend/core/training/training.py | 376 ++++++++++++-- studio/backend/core/training/worker.py | 6 +- studio/backend/main.py | 15 + studio/backend/models/__init__.py | 10 + studio/backend/models/training.py | 60 ++- studio/backend/routes/__init__.py | 2 + studio/backend/routes/training.py | 61 ++- studio/backend/routes/training_history.py | 85 ++++ studio/backend/storage/__init__.py | 2 + studio/backend/storage/studio_db.py | 362 +++++++++++++ studio/backend/utils/downsample.py | 18 + studio/backend/utils/paths/__init__.py | 2 + studio/backend/utils/paths/storage_roots.py | 4 + .../studio/historical-training-view.tsx | 168 ++++++ .../src/features/studio/history-card-grid.tsx | 401 +++++++++++++++ .../features/studio/live-training-view.tsx | 118 +++++ .../studio/sections/charts-section.tsx | 45 +- .../studio/sections/progress-section.tsx | 477 ++++++++++-------- .../src/features/studio/studio-page.tsx | 142 ++++-- .../src/features/studio/training-view.tsx | 57 --- .../src/features/training/api/history-api.ts | 59 +++ .../frontend/src/features/training/index.ts | 10 +- .../src/features/training/types/history.ts | 48 ++ .../src/features/training/types/runtime.ts | 33 +- 25 files changed, 2156 insertions(+), 413 deletions(-) create mode 100644 studio/backend/routes/training_history.py create mode 100644 studio/backend/storage/__init__.py create mode 100644 studio/backend/storage/studio_db.py create mode 100644 studio/backend/utils/downsample.py create mode 100644 studio/frontend/src/features/studio/historical-training-view.tsx create mode 100644 studio/frontend/src/features/studio/history-card-grid.tsx create mode 100644 studio/frontend/src/features/studio/live-training-view.tsx delete mode 100644 studio/frontend/src/features/studio/training-view.tsx create mode 100644 studio/frontend/src/features/training/api/history-api.ts create mode 100644 studio/frontend/src/features/training/types/history.ts diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 5f504bbdf4..2324916236 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -81,8 +81,8 @@ class TrainingProgress: epoch: float = 0 step: int = 0 total_steps: int = 0 - loss: float = 0.0 - learning_rate: float = 0.0 + loss: Optional[float] = None + learning_rate: Optional[float] = None is_training: bool = False is_completed: bool = False error: Optional[str] = None @@ -244,7 +244,7 @@ class UnslothTrainer: def on_log(self, args, state, control, logs = None, **kwargs): if not logs: return - loss_value = logs.get("loss", logs.get("train_loss", 0.0)) + loss_value = logs.get("loss", logs.get("train_loss", None)) current_step = state.global_step grad_norm = logs.get("grad_norm", None) @@ -268,7 +268,7 @@ class UnslothTrainer: step = current_step, epoch = round(state.epoch, 2) if state.epoch else 0, loss = loss_value, - learning_rate = logs.get("learning_rate", 0.0), + learning_rate = logs.get("learning_rate", None), elapsed_seconds = elapsed_seconds, eta_seconds = eta_seconds, grad_norm = grad_norm, diff --git a/studio/backend/core/training/training.py b/studio/backend/core/training/training.py index 9626f9df2e..4439e4e173 100644 --- a/studio/backend/core/training/training.py +++ b/studio/backend/core/training/training.py @@ -14,12 +14,14 @@ worker's mp.Queue, and exposes the same API surface to routes/training.py. Pattern follows core/data_recipe/jobs/manager.py. """ +import json as _json import math import multiprocessing as mp import queue import threading import time import structlog +from datetime import datetime, timezone from loggers import get_logger from dataclasses import dataclass, field from pathlib import Path @@ -44,8 +46,8 @@ class TrainingProgress: epoch: float = 0 step: int = 0 total_steps: int = 0 - loss: float = 0.0 - learning_rate: float = 0.0 + loss: Optional[float] = None + learning_rate: Optional[float] = None is_training: bool = False is_completed: bool = False error: Optional[str] = None @@ -63,6 +65,8 @@ class TrainingBackend: Launches a fresh subprocess per training job, communicates via mp.Queue. """ + FLUSH_THRESHOLD: int = 10 + def __init__(self): # Subprocess state self._proc: Optional[mp.Process] = None @@ -91,13 +95,21 @@ class TrainingBackend: self.current_job_id: Optional[str] = None self._output_dir: Optional[str] = None + # DB persistence + self._metric_buffer: list[dict] = [] + self._run_finalized: bool = False + self._db_run_created: bool = False + self._db_total_steps_set: bool = False + self._db_config: Optional[dict] = None + self._db_started_at: Optional[str] = None + logger.info("TrainingBackend initialized (subprocess mode)") # ------------------------------------------------------------------ # Public API (called by routes/training.py) # ------------------------------------------------------------------ - def start_training(self, **kwargs) -> bool: + def start_training(self, job_id: str, **kwargs) -> bool: """Spawn a subprocess to run the full training pipeline. All kwargs are serialized into a config dict and sent to the worker. @@ -108,30 +120,16 @@ class TrainingBackend: logger.warning("Training subprocess already running") return False - # Join prior pump thread to prevent it from consuming events - # from the new job's queue (it reads self._event_queue dynamically). + # Join prior pump thread — refuse to start if it won't die if self._pump_thread is not None and self._pump_thread.is_alive(): self._pump_thread.join(timeout = 5.0) if self._pump_thread.is_alive(): - logger.warning("Previous pump thread did not exit within 5s") + logger.warning( + "Previous pump thread did not exit within 5s — refusing to start" + ) + return False self._pump_thread = None - # Reset state - self._should_stop = False - self._cancel_requested = False - self._progress = TrainingProgress( - is_training = True, status_message = "Initializing training..." - ) - self.loss_history.clear() - self.lr_history.clear() - self.step_history.clear() - self.grad_norm_history.clear() - self.grad_norm_step_history.clear() - self.eval_loss_history.clear() - self.eval_step_history.clear() - self.eval_enabled = False - self._output_dir = None - # Build config dict for the subprocess config = { "model_name": kwargs["model_name"], @@ -193,23 +191,62 @@ class TrainingBackend: if config["training_type"] != "LoRA/QLoRA": config["load_in_4bit"] = False - # Spawn subprocess + # Spawn subprocess — use locals so state is untouched on failure from .worker import run_training_process - self._event_queue = _CTX.Queue() - self._stop_queue = _CTX.Queue() + event_queue = _CTX.Queue() + stop_queue = _CTX.Queue() - self._proc = _CTX.Process( + proc = _CTX.Process( target = run_training_process, kwargs = { - "event_queue": self._event_queue, - "stop_queue": self._stop_queue, + "event_queue": event_queue, + "stop_queue": stop_queue, "config": config, }, daemon = True, ) - self._proc.start() - logger.info("Training subprocess started (pid=%s)", self._proc.pid) + try: + proc.start() + except Exception: + logger.error("Failed to start training subprocess", exc_info = True) + return False + + logger.info("Training subprocess started (pid=%s)", proc.pid) + + # Reset state — safe because old pump thread is confirmed dead + # and proc.start() succeeded + self.current_job_id = job_id + self._should_stop = False + self._cancel_requested = False + self._progress = TrainingProgress( + is_training = True, status_message = "Initializing training..." + ) + self.loss_history.clear() + self.lr_history.clear() + self.step_history.clear() + self.grad_norm_history.clear() + self.grad_norm_step_history.clear() + self.eval_loss_history.clear() + self.eval_step_history.clear() + self.eval_enabled = False + self._output_dir = None + self._metric_buffer.clear() + self._run_finalized = False + self._db_run_created = False + self._db_total_steps_set = False + self._db_config = { + k: v for k, v in config.items() if k not in {"hf_token", "wandb_token"} + } + self._db_started_at = datetime.now(timezone.utc).isoformat() + + # Assign subprocess handles after state reset + self._event_queue = event_queue + self._stop_queue = stop_queue + self._proc = proc + + # Eagerly create DB run row so the run appears in history during model loading + self._ensure_db_run_created() # Start event pump thread self._pump_thread = threading.Thread(target = self._pump_loop, daemon = True) @@ -252,6 +289,11 @@ class TrainingBackend: proc.kill() proc.join(timeout = 2.0) + # Wait for pump thread to finish DB finalization before returning + # (8s covers SQLite's default 5s lock timeout plus execution overhead) + if self._pump_thread is not None and self._pump_thread.is_alive(): + self._pump_thread.join(timeout = 8.0) + def is_training_active(self) -> bool: """Check if training is currently active.""" with self._lock: @@ -389,20 +431,54 @@ class TrainingBackend: self._progress.error or "Training process exited unexpectedly" ) + + self._ensure_db_run_created() + self._finalize_run_in_db( + status = "stopped" if self._should_stop else "error", + error_message = None + if self._should_stop + else "Training process terminated unexpectedly", + ) return def _handle_event(self, event: dict) -> None: - """Apply a subprocess event to local state.""" + """Apply a subprocess event to local state. + + State updates happen inside self._lock; DB I/O happens after + releasing it so status-polling API endpoints are never blocked + by slow SQLite writes. + """ etype = event.get("type") + db_action: Optional[str] = None + db_action_kwargs: dict = {} with self._lock: if etype == "progress": self._progress.step = event.get("step", self._progress.step) self._progress.epoch = event.get("epoch", self._progress.epoch) - self._progress.loss = event.get("loss", self._progress.loss) - self._progress.learning_rate = event.get( - "learning_rate", self._progress.learning_rate - ) + # loss/lr are sanitized below; update progress after coercion + _raw_loss = event.get("loss") + _raw_lr = event.get("learning_rate") + try: + _safe_loss = float(_raw_loss) if _raw_loss is not None else None + except (TypeError, ValueError): + logger.debug("Could not convert loss to float: %s", _raw_loss) + _safe_loss = None + if _safe_loss is not None and not math.isfinite(_safe_loss): + _safe_loss = None + try: + _safe_lr = float(_raw_lr) if _raw_lr is not None else None + except (TypeError, ValueError): + logger.debug( + "Could not convert learning_rate to float: %s", _raw_lr + ) + _safe_lr = None + if _safe_lr is not None and not math.isfinite(_safe_lr): + _safe_lr = None + if _safe_loss is not None: + self._progress.loss = _safe_loss + if _safe_lr is not None: + self._progress.learning_rate = _safe_lr self._progress.total_steps = event.get( "total_steps", self._progress.total_steps ) @@ -416,30 +492,85 @@ class TrainingBackend: if status: self._progress.status_message = status - # Update metric histories + # Update metric histories — reuse sanitized values from above step = event.get("step", 0) - loss = event.get("loss", 0.0) - lr = event.get("learning_rate", 0.0) - if step >= 0 and loss > 0: + loss = _safe_loss + lr = _safe_lr + if step > 0 and loss is not None: self.loss_history.append(loss) - self.lr_history.append(lr) + self.lr_history.append(lr if lr is not None else 0.0) self.step_history.append(step) grad_norm = event.get("grad_norm") + gn = None if grad_norm is not None: try: gn = float(grad_norm) except (TypeError, ValueError): gn = None - if gn is not None and math.isfinite(gn): + if step > 0 and gn is not None and math.isfinite(gn): self.grad_norm_history.append(gn) self.grad_norm_step_history.append(step) + else: + gn = None eval_loss = event.get("eval_loss") if eval_loss is not None: - self.eval_loss_history.append(eval_loss) - self.eval_step_history.append(step) - self.eval_enabled = True + try: + eval_loss = float(eval_loss) + except (TypeError, ValueError): + logger.debug( + "Could not convert eval_loss to float: %s", eval_loss + ) + eval_loss = None + if step > 0 and eval_loss is not None and math.isfinite(eval_loss): + self.eval_loss_history.append(eval_loss) + self.eval_step_history.append(step) + self.eval_enabled = True + else: + eval_loss = None + + # Buffer metric for DB flush (loss/lr already sanitized above) + self._metric_buffer.append( + { + "step": step, + "loss": loss, + "learning_rate": lr, + "grad_norm": gn, + "eval_loss": eval_loss, + "epoch": event.get("epoch"), + "num_tokens": event.get("num_tokens"), + "elapsed_seconds": event.get("elapsed_seconds"), + } + ) + + # Decide which DB action to take after releasing the lock + if not self._db_run_created and self.current_job_id and self._db_config: + db_action = "create_run" + db_action_kwargs = { + "job_id": self.current_job_id, + "model_name": self._db_config["model_name"], + "dataset_name": self._db_config.get("hf_dataset") + or next( + iter(self._db_config.get("local_datasets") or []), "unknown" + ), + "config_json": _json.dumps(self._db_config), + "started_at": self._db_started_at + or datetime.now(timezone.utc).isoformat(), + "total_steps": event.get("total_steps"), + } + elif ( + event.get("total_steps") + and self._db_run_created + and not self._db_total_steps_set + ): + db_action = "update_total_steps" + db_action_kwargs = { + "job_id": self.current_job_id, + "total_steps": event["total_steps"], + } + elif len(self._metric_buffer) >= self.FLUSH_THRESHOLD: + db_action = "flush" elif etype == "eval_configured": self.eval_enabled = True @@ -454,6 +585,14 @@ class TrainingBackend: self._output_dir = event.get("output_dir") msg = event.get("status_message", "Training completed") self._progress.status_message = msg + if not self._db_run_created and self.current_job_id and self._db_config: + db_action = "create_and_finalize" + else: + db_action = "finalize" + db_action_kwargs = { + "status": "stopped" if self._should_stop else "completed", + "output_dir": self._output_dir, + } elif etype == "error": self._progress.is_training = False @@ -462,6 +601,149 @@ class TrainingBackend: stack = event.get("stack", "") if stack: logger.error("Stack trace:\n%s", stack) + if not self._db_run_created and self.current_job_id and self._db_config: + db_action = "create_and_finalize" + else: + db_action = "finalize" + db_action_kwargs = { + "status": "stopped" if self._should_stop else "error", + "error_message": event.get("error", "Unknown error"), + } + + # --- DB I/O outside the lock --- + if db_action == "create_run": + try: + from storage.studio_db import create_run + + create_run( + id = db_action_kwargs["job_id"], + model_name = db_action_kwargs["model_name"], + dataset_name = db_action_kwargs["dataset_name"], + config_json = db_action_kwargs["config_json"], + started_at = db_action_kwargs["started_at"], + total_steps = db_action_kwargs["total_steps"], + ) + self._db_run_created = True + if db_action_kwargs["total_steps"]: + self._db_total_steps_set = True + except Exception: + logger.warning("Failed to create DB run record", exc_info = True) + elif db_action == "create_and_finalize": + self._ensure_db_run_created() + self._finalize_run_in_db(**db_action_kwargs) + elif db_action == "update_total_steps": + try: + from storage.studio_db import update_run_total_steps + + update_run_total_steps( + db_action_kwargs["job_id"], db_action_kwargs["total_steps"] + ) + self._db_total_steps_set = True + except Exception: + logger.warning("Failed to update total_steps in DB", exc_info = True) + elif db_action == "flush": + self._flush_metrics_to_db() + elif db_action == "finalize": + self._finalize_run_in_db(**db_action_kwargs) + + def _ensure_db_run_created(self) -> None: + """Create the DB row if it doesn't exist yet. Called outside the lock.""" + if self._db_run_created or not self.current_job_id or not self._db_config: + return + try: + from storage.studio_db import create_run + + dataset_name = self._db_config.get("hf_dataset") or next( + iter(self._db_config.get("local_datasets") or []), "unknown" + ) + create_run( + id = self.current_job_id, + model_name = self._db_config["model_name"], + dataset_name = dataset_name, + config_json = _json.dumps(self._db_config), + started_at = self._db_started_at + or datetime.now(timezone.utc).isoformat(), + total_steps = self._progress.total_steps or None, + ) + self._db_run_created = True + except Exception: + logger.warning( + "Failed to create DB run record for early failure", exc_info = True + ) + + def _finalize_run_in_db( + self, + status: str, + error_message: Optional[str] = None, + output_dir: Optional[str] = None, + ) -> None: + """Flush remaining metrics and mark a run as finished in the DB.""" + if not self.current_job_id or not self._db_run_created or self._run_finalized: + return + self._flush_metrics_to_db() + try: + from storage.studio_db import finish_run + from utils.downsample import downsample + + sparkline = downsample(self.loss_history, 50) + finish_run( + id = self.current_job_id, + status = status, + ended_at = datetime.now(timezone.utc).isoformat(), + final_step = self._progress.step, + final_loss = self._progress.loss + if ( + self._progress.loss is not None + and math.isfinite(self._progress.loss) + ) + else None, + duration_seconds = self._progress.elapsed_seconds, + loss_sparkline = _json.dumps(sparkline), + output_dir = output_dir, + error_message = error_message, + ) + self._run_finalized = True + except Exception: + logger.warning( + "Failed to finalize run in DB (status=%s)", status, exc_info = True + ) + + def _flush_metrics_to_db(self) -> None: + """Flush buffered metrics to the database and update live progress.""" + if ( + not self._metric_buffer + or not self.current_job_id + or not self._db_run_created + ): + return + # Cap buffer to prevent unbounded memory growth + if len(self._metric_buffer) > 500: + logger.warning( + "Metric buffer exceeded 500 entries (%d) — trimming oldest", + len(self._metric_buffer), + ) + self._metric_buffer = self._metric_buffer[-500:] + # Snapshot before insert so metrics arriving during the write are preserved + batch = list(self._metric_buffer) + try: + from storage.studio_db import insert_metrics_batch, update_run_progress + + insert_metrics_batch(self.current_job_id, batch) + del self._metric_buffer[: len(batch)] + update_run_progress( + id = self.current_job_id, + step = self._progress.step, + loss = self._progress.loss + if ( + self._progress.loss is not None + and math.isfinite(self._progress.loss) + ) + else None, + duration_seconds = self._progress.elapsed_seconds, + ) + except Exception: + # Leave buffer intact for retry on next flush + logger.warning("Failed to flush metrics to DB", exc_info = True) @staticmethod def _read_queue(q: Any, timeout_sec: float) -> Optional[dict]: @@ -561,11 +843,13 @@ class TrainingBackend: if progress.error: title = f"Error: {progress.error}" elif progress.is_completed: - title = f"Training completed! Final loss: {progress.loss:.4f}" + loss_str = f"{progress.loss:.4f}" if progress.loss is not None else "--" + title = f"Training completed! Final loss: {loss_str}" elif progress.status_message: title = progress.status_message elif progress.step > 0: - title = f"Epoch: {progress.epoch} | Step: {progress.step}/{progress.total_steps} | Loss: {progress.loss:.4f}" + loss_str = f"{progress.loss:.4f}" if progress.loss is not None else "--" + title = f"Epoch: {progress.epoch} | Step: {progress.step}/{progress.total_steps} | Loss: {loss_str}" else: title = "Training Loss" diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py index ccd805b7ac..d06dd6d358 100644 --- a/studio/backend/core/training/worker.py +++ b/studio/backend/core/training/worker.py @@ -242,7 +242,7 @@ def run_training_process( # Wire up progress callback → event_queue def _on_progress(progress: TrainingProgress): - has_train_loss = progress.step >= 0 and progress.loss > 0 + has_train_loss = progress.step > 0 and progress.loss is not None has_eval_loss = progress.eval_loss is not None if has_train_loss or has_eval_loss: event_queue.put( @@ -918,7 +918,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) -> def on_log(self, args, state, control, logs = None, **kwargs): if not logs: return - loss_value = logs.get("loss", logs.get("train_loss", 0.0)) + loss_value = logs.get("loss", logs.get("train_loss", None)) current_step = state.global_step elapsed = time.time() - training_start_time @@ -934,7 +934,7 @@ def _run_embedding_training(event_queue: Any, stop_queue: Any, config: dict) -> "step": current_step, "epoch": round(state.epoch, 2) if state.epoch else 0, "loss": loss_value, - "learning_rate": logs.get("learning_rate", 0.0), + "learning_rate": logs.get("learning_rate", None), "total_steps": total_steps, "elapsed_seconds": elapsed, "eta_seconds": eta, diff --git a/studio/backend/main.py b/studio/backend/main.py index 7134c5a783..5e647f6312 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -49,6 +49,7 @@ from routes import ( export_router, inference_router, models_router, + training_history_router, training_router, ) from auth import storage @@ -73,6 +74,17 @@ async def lifespan(app: FastAPI): # Detect hardware first — sets DEVICE global used everywhere detect_hardware() + from storage.studio_db import cleanup_orphaned_runs + + try: + cleanup_orphaned_runs() + except Exception as exc: + import structlog + + structlog.get_logger(__name__).warning( + "cleanup_orphaned_runs failed at startup: %s", exc + ) + # Pre-cache the helper GGUF model for LLM-assisted dataset detection. # Runs in a background thread so it doesn't block server startup. import threading @@ -149,6 +161,9 @@ app.include_router(inference_router, prefix = "/v1", tags = ["openai-compat"]) app.include_router(datasets_router, prefix = "/api/datasets", tags = ["datasets"]) app.include_router(data_recipe_router, prefix = "/api/data-recipe", tags = ["data-recipe"]) app.include_router(export_router, prefix = "/api/export", tags = ["export"]) +app.include_router( + training_history_router, prefix = "/api/train", tags = ["training-history"] +) # ============ Health and System Endpoints ============ diff --git a/studio/backend/models/__init__.py b/studio/backend/models/__init__.py index 11cf215f54..a4fbbbe6ee 100644 --- a/studio/backend/models/__init__.py +++ b/studio/backend/models/__init__.py @@ -10,6 +10,11 @@ from .training import ( TrainingJobResponse, TrainingStatus, TrainingProgress, + TrainingRunSummary, + TrainingRunListResponse, + TrainingRunMetrics, + TrainingRunDetailResponse, + TrainingRunDeleteResponse, ) from .models import ( CheckpointInfo, @@ -71,6 +76,11 @@ __all__ = [ "TrainingJobResponse", "TrainingStatus", "TrainingProgress", + "TrainingRunSummary", + "TrainingRunListResponse", + "TrainingRunMetrics", + "TrainingRunDetailResponse", + "TrainingRunDeleteResponse", # Model management schemas "ModelDetails", "LocalModelInfo", diff --git a/studio/backend/models/training.py b/studio/backend/models/training.py index 342e44cc09..68791aa7a8 100644 --- a/studio/backend/models/training.py +++ b/studio/backend/models/training.py @@ -177,8 +177,8 @@ class TrainingProgress(BaseModel): job_id: str = Field(..., description = "Training job identifier") step: int = Field(..., description = "Current training step") total_steps: int = Field(..., description = "Total training steps") - loss: float = Field(..., description = "Current loss value") - learning_rate: float = Field(..., description = "Current learning rate") + loss: Optional[float] = Field(None, description = "Current loss value") + learning_rate: Optional[float] = Field(None, description = "Current learning rate") progress_percent: float = Field( ..., description = "Progress percentage (0.0 to 100.0)" ) @@ -196,3 +196,59 @@ class TrainingProgress(BaseModel): eval_loss: Optional[float] = Field( None, description = "Eval loss from the most recent evaluation step" ) + + +class TrainingRunSummary(BaseModel): + """Summary of a training run for list views.""" + + id: str + status: Literal["running", "completed", "stopped", "error"] + model_name: str + dataset_name: str + started_at: str + ended_at: Optional[str] = None + total_steps: Optional[int] = None + final_step: Optional[int] = None + final_loss: Optional[float] = None + output_dir: Optional[str] = None + duration_seconds: Optional[float] = None + error_message: Optional[str] = None + loss_sparkline: Optional[List[float]] = None + + +class TrainingRunListResponse(BaseModel): + """Response for listing training runs.""" + + runs: List[TrainingRunSummary] + total: int + + +class TrainingRunMetrics(BaseModel): + """Metrics arrays for a training run, using paired step arrays per metric.""" + + step_history: List[int] = Field(default_factory = list) + loss_history: List[float] = Field(default_factory = list) + loss_step_history: List[int] = Field(default_factory = list) + lr_history: List[float] = Field(default_factory = list) + lr_step_history: List[int] = Field(default_factory = list) + grad_norm_history: List[float] = Field(default_factory = list) + grad_norm_step_history: List[int] = Field(default_factory = list) + eval_loss_history: List[float] = Field(default_factory = list) + eval_step_history: List[int] = Field(default_factory = list) + final_epoch: Optional[float] = None + final_num_tokens: Optional[int] = None + + +class TrainingRunDetailResponse(BaseModel): + """Response for a single training run with config and metrics.""" + + run: TrainingRunSummary + config: dict + metrics: TrainingRunMetrics + + +class TrainingRunDeleteResponse(BaseModel): + """Response for deleting a training run.""" + + status: str + message: str diff --git a/studio/backend/routes/__init__.py b/studio/backend/routes/__init__.py index b45eff821b..e79f6553f9 100644 --- a/studio/backend/routes/__init__.py +++ b/studio/backend/routes/__init__.py @@ -12,6 +12,7 @@ from routes.datasets import router as datasets_router from routes.auth import router as auth_router from routes.data_recipe import router as data_recipe_router from routes.export import router as export_router +from routes.training_history import router as training_history_router __all__ = [ "training_router", @@ -21,4 +22,5 @@ __all__ = [ "auth_router", "data_recipe_router", "export_router", + "training_history_router", ] diff --git a/studio/backend/routes/training.py b/studio/backend/routes/training.py index 4f8054f80e..4cfb060dee 100644 --- a/studio/backend/routes/training.py +++ b/studio/backend/routes/training.py @@ -14,6 +14,7 @@ import structlog from loggers import get_logger import asyncio from datetime import datetime +import uuid as _uuid # Add backend directory to path # The backend code should be in the same directory structure @@ -115,15 +116,11 @@ async def start_training( backend = get_training_backend() - # Generate job ID and attach to backend for later status/progress calls - job_id = f"job_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - backend.current_job_id = job_id - - # Check if training is already active + # Check if training is already active (before mutating any state) if backend.is_training_active(): existing_job_id: Optional[str] = getattr(backend, "current_job_id", "") return TrainingJobResponse( - job_id = existing_job_id or job_id, + job_id = existing_job_id or "", status = "error", message = ( "Training is already in progress. " @@ -132,6 +129,12 @@ async def start_training( error = "Training already active", ) + # Generate job ID — passed into start_training() which sets it on the + # backend only after confirming the old pump thread is dead. + job_id = ( + f"job_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{_uuid.uuid4().hex[:8]}" + ) + # Validate dataset paths if provided if request.local_datasets: request.local_datasets = _validate_local_dataset_paths( @@ -248,12 +251,12 @@ async def start_training( logger.warning("Could not shut down export subprocess: %s", e) # start_training now spawns a subprocess (non-blocking) - success = backend.start_training(**training_kwargs) + success = backend.start_training(job_id = job_id, **training_kwargs) if not success: progress_error = backend.trainer.training_progress.error return TrainingJobResponse( - job_id = job_id, + job_id = backend.current_job_id or "", status = "error", message = progress_error or "Failed to start training subprocess", error = progress_error or "subprocess_start_failed", @@ -345,7 +348,7 @@ async def reset_training( error = None, status_message = "Ready to train", step = 0, - loss = 0.0, + loss = None, epoch = 0, total_steps = 0, ) @@ -419,8 +422,8 @@ async def get_training_status( "epoch": getattr(progress, "epoch", 0), "step": getattr(progress, "step", 0), "total_steps": getattr(progress, "total_steps", 0), - "loss": getattr(progress, "loss", 0.0), - "learning_rate": getattr(progress, "learning_rate", 0.0), + "loss": getattr(progress, "loss", None), + "learning_rate": getattr(progress, "learning_rate", None), } # Build metric history for chart recovery after SSE reconnection @@ -526,8 +529,8 @@ async def stream_training_progress( # ── Helpers ────────────────────────────────────────────── def build_progress( step: int, - loss: float, - learning_rate: float, + loss: Optional[float], + learning_rate: Optional[float], total_steps: int, epoch: Optional[float] = None, progress: Optional[Any] = None, @@ -604,10 +607,10 @@ async def stream_training_progress( loss_val = ( backend.loss_history[i] if i < len(backend.loss_history) - else 0.0 + else None ) lr_val = ( - backend.lr_history[i] if i < len(backend.lr_history) else 0.0 + backend.lr_history[i] if i < len(backend.lr_history) else None ) tp_replay = getattr( getattr(backend, "trainer", None), "training_progress", None @@ -645,8 +648,8 @@ async def stream_training_progress( initial_progress = build_progress( step = 0, - loss = 0.0, - learning_rate = 0.0, + loss = None, + learning_rate = None, total_steps = initial_total_steps, epoch = initial_epoch, progress = tp, @@ -660,9 +663,9 @@ async def stream_training_progress( if backend.step_history: final_step = backend.step_history[-1] final_loss = ( - backend.loss_history[-1] if backend.loss_history else 0.0 + backend.loss_history[-1] if backend.loss_history else None ) - final_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + final_lr = backend.lr_history[-1] if backend.lr_history else None final_total_steps = ( getattr(tp, "total_steps", final_step) if tp else final_step ) @@ -680,7 +683,9 @@ async def stream_training_progress( ) else: yield format_sse( - build_progress(-1, 0.0, 0.0, 0, progress = tp).model_dump_json(), + build_progress( + -1, None, None, 0, progress = tp + ).model_dump_json(), event = "complete", event_id = 0, ) @@ -698,9 +703,9 @@ async def stream_training_progress( if backend.step_history: current_step = backend.step_history[-1] current_loss = ( - backend.loss_history[-1] if backend.loss_history else 0.0 + backend.loss_history[-1] if backend.loss_history else None ) - current_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + current_lr = backend.lr_history[-1] if backend.lr_history else None tp_inner = getattr( getattr(backend, "trainer", None), "training_progress", None ) @@ -763,8 +768,8 @@ async def stream_training_progress( ) preparing_payload = build_progress( 0, - 0.0, - 0.0, + None, + None, prep_total, progress = tp_prep, ) @@ -781,7 +786,7 @@ async def stream_training_progress( getattr(backend, "trainer", None), "training_progress", None ) timeout_payload = build_progress( - last_step, 0.0, 0.0, 0, progress = tp_timeout + last_step, None, None, 0, progress = tp_timeout ) yield format_sse( timeout_payload.model_dump_json(), @@ -797,7 +802,7 @@ async def stream_training_progress( tp_error = getattr( getattr(backend, "trainer", None), "training_progress", None ) - error_payload = build_progress(0, 0.0, 0.0, 0, progress = tp_error) + error_payload = build_progress(0, None, None, 0, progress = tp_error) yield format_sse( error_payload.model_dump_json(), event = "error", @@ -807,8 +812,8 @@ async def stream_training_progress( # ── Final "complete" event ─────────────────────────────── final_step = backend.step_history[-1] if backend.step_history else last_step - final_loss = backend.loss_history[-1] if backend.loss_history else 0.0 - final_lr = backend.lr_history[-1] if backend.lr_history else 0.0 + final_loss = backend.loss_history[-1] if backend.loss_history else None + final_lr = backend.lr_history[-1] if backend.lr_history else None final_tp = getattr(getattr(backend, "trainer", None), "training_progress", None) final_total_steps = ( getattr(final_tp, "total_steps", final_step) if final_tp else final_step diff --git a/studio/backend/routes/training_history.py b/studio/backend/routes/training_history.py new file mode 100644 index 0000000000..597c4424c0 --- /dev/null +++ b/studio/backend/routes/training_history.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +""" +Training history API routes — browse, view, and delete past training runs. +""" + +import json + +from fastapi import APIRouter, Depends, HTTPException, Query +from loggers import get_logger + +from auth.authentication import get_current_subject +from models import ( + TrainingRunDeleteResponse, + TrainingRunDetailResponse, + TrainingRunListResponse, + TrainingRunMetrics, + TrainingRunSummary, +) +from storage.studio_db import delete_run, get_run, get_run_metrics, list_runs + +logger = get_logger(__name__) + +router = APIRouter() + + +@router.get("/runs", response_model = TrainingRunListResponse) +async def list_training_runs( + limit: int = Query(50, ge = 1, le = 200), + offset: int = Query(0, ge = 0), + current_subject: str = Depends(get_current_subject), +): + """List training runs, newest first.""" + result = list_runs(limit = limit, offset = offset) + return TrainingRunListResponse( + runs = [TrainingRunSummary(**r) for r in result["runs"]], + total = result["total"], + ) + + +@router.get("/runs/{run_id}", response_model = TrainingRunDetailResponse) +async def get_training_run_detail( + run_id: str, + current_subject: str = Depends(get_current_subject), +): + """Get a single training run with full config and metrics.""" + run = get_run(run_id) + if run is None: + raise HTTPException(status_code = 404, detail = f"Run {run_id} not found") + + try: + config = json.loads(run.get("config_json", "{}")) + except (json.JSONDecodeError, TypeError): + logger.debug("Failed to parse config_json for run %s", run_id) + config = {} + + metrics_data = get_run_metrics(run_id) + + return TrainingRunDetailResponse( + run = TrainingRunSummary(**{k: v for k, v in run.items() if k != "config_json"}), + config = config, + metrics = TrainingRunMetrics(**metrics_data), + ) + + +@router.delete("/runs/{run_id}", response_model = TrainingRunDeleteResponse) +async def delete_training_run( + run_id: str, + current_subject: str = Depends(get_current_subject), +): + """Delete a training run and its metrics (CASCADE).""" + run = get_run(run_id) + if run is None: + raise HTTPException(status_code = 404, detail = f"Run {run_id} not found") + if run["status"] == "running": + raise HTTPException( + status_code = 409, detail = "Cannot delete a running training run" + ) + logger.info("Deleting training run %s", run_id) + delete_run(run_id) + return TrainingRunDeleteResponse( + status = "deleted", + message = f"Run {run_id} deleted", + ) diff --git a/studio/backend/storage/__init__.py b/studio/backend/storage/__init__.py new file mode 100644 index 0000000000..32014236c6 --- /dev/null +++ b/studio/backend/storage/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 diff --git a/studio/backend/storage/studio_db.py b/studio/backend/storage/studio_db.py new file mode 100644 index 0000000000..4af19df42b --- /dev/null +++ b/studio/backend/storage/studio_db.py @@ -0,0 +1,362 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +""" +SQLite storage for training run history and metrics. + +Follows the same pattern as auth/storage.py — module-level functions, +raw sqlite3, per-function connections. Enhancements over auth: + - WAL mode for concurrent read/write access + - PRAGMA foreign_keys = ON for CASCADE deletes +""" + +import json +import logging +import sqlite3 +import threading + +logger = logging.getLogger(__name__) +from typing import Optional + +from utils.paths import studio_db_path, ensure_dir + +_schema_lock = threading.Lock() +_schema_ready = False + + +def _ensure_schema(conn: sqlite3.Connection) -> None: + """Create tables and indexes if they don't exist. Called once per process.""" + conn.execute("PRAGMA journal_mode=WAL") + conn.execute( + """ + CREATE TABLE IF NOT EXISTS training_runs ( + id TEXT NOT NULL PRIMARY KEY, + status TEXT NOT NULL DEFAULT 'running', + model_name TEXT NOT NULL, + dataset_name TEXT NOT NULL, + config_json TEXT NOT NULL, + started_at TEXT NOT NULL, + ended_at TEXT, + total_steps INTEGER, + final_step INTEGER, + final_loss REAL, + output_dir TEXT, + error_message TEXT, + duration_seconds REAL, + loss_sparkline TEXT + ) + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS training_metrics ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + run_id TEXT NOT NULL REFERENCES training_runs(id) ON DELETE CASCADE, + step INTEGER NOT NULL, + loss REAL, + learning_rate REAL, + grad_norm REAL, + eval_loss REAL, + epoch REAL, + num_tokens INTEGER, + elapsed_seconds REAL, + UNIQUE(run_id, step) + ) + """ + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_metrics_run_id ON training_metrics(run_id)" + ) + + +def get_connection() -> sqlite3.Connection: + """Open studio.db with WAL mode, create tables once per process, enable foreign keys.""" + global _schema_ready + db_path = studio_db_path() + ensure_dir(db_path.parent) + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + # foreign_keys is session-scoped, must be set per connection + conn.execute("PRAGMA foreign_keys=ON") + if not _schema_ready: + with _schema_lock: + if not _schema_ready: + try: + _ensure_schema(conn) + _schema_ready = True + except Exception: + conn.close() + raise + return conn + + +def create_run( + id: str, + model_name: str, + dataset_name: str, + config_json: str, + started_at: str, + total_steps: Optional[int], +) -> None: + conn = get_connection() + try: + conn.execute( + """ + INSERT INTO training_runs (id, model_name, dataset_name, config_json, started_at, total_steps) + VALUES (?, ?, ?, ?, ?, ?) + """, + (id, model_name, dataset_name, config_json, started_at, total_steps), + ) + conn.commit() + finally: + conn.close() + + +def update_run_total_steps(id: str, total_steps: int) -> None: + conn = get_connection() + try: + conn.execute( + "UPDATE training_runs SET total_steps = ? WHERE id = ?", + (total_steps, id), + ) + conn.commit() + finally: + conn.close() + + +def update_run_progress( + id: str, step: int, loss: Optional[float], duration_seconds: Optional[float] +) -> None: + """Update current progress on a running training run (called on each metric flush).""" + conn = get_connection() + try: + conn.execute( + "UPDATE training_runs SET final_step = ?, final_loss = ?, duration_seconds = ? WHERE id = ?", + (step, loss, duration_seconds, id), + ) + conn.commit() + finally: + conn.close() + + +def finish_run( + id: str, + status: str, + ended_at: str, + final_step: Optional[int], + final_loss: Optional[float], + duration_seconds: Optional[float], + loss_sparkline: Optional[str] = None, + output_dir: Optional[str] = None, + error_message: Optional[str] = None, +) -> None: + conn = get_connection() + try: + conn.execute( + """ + UPDATE training_runs + SET status = ?, ended_at = ?, final_step = ?, final_loss = ?, + duration_seconds = ?, loss_sparkline = ?, output_dir = ?, + error_message = ? + WHERE id = ? + """, + ( + status, + ended_at, + final_step, + final_loss, + duration_seconds, + loss_sparkline, + output_dir, + error_message, + id, + ), + ) + conn.commit() + finally: + conn.close() + + +def insert_metrics_batch(run_id: str, metrics: list[dict]) -> None: + if not metrics: + return + conn = get_connection() + try: + conn.executemany( + """ + INSERT INTO training_metrics + (run_id, step, loss, learning_rate, grad_norm, eval_loss, epoch, num_tokens, elapsed_seconds) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(run_id, step) DO UPDATE SET + loss = COALESCE(excluded.loss, loss), + learning_rate = COALESCE(excluded.learning_rate, learning_rate), + grad_norm = COALESCE(excluded.grad_norm, grad_norm), + eval_loss = COALESCE(excluded.eval_loss, eval_loss), + epoch = COALESCE(excluded.epoch, epoch), + num_tokens = COALESCE(excluded.num_tokens, num_tokens), + elapsed_seconds = COALESCE(excluded.elapsed_seconds, elapsed_seconds) + """, + [ + ( + run_id, + m.get("step"), + m.get("loss"), + m.get("learning_rate"), + m.get("grad_norm"), + m.get("eval_loss"), + m.get("epoch"), + m.get("num_tokens"), + m.get("elapsed_seconds"), + ) + for m in metrics + ], + ) + conn.commit() + finally: + conn.close() + + +def list_runs(limit: int = 50, offset: int = 0) -> dict: + conn = get_connection() + try: + total = conn.execute("SELECT COUNT(*) FROM training_runs").fetchone()[0] + rows = conn.execute( + """ + SELECT id, status, model_name, dataset_name, started_at, ended_at, + total_steps, final_step, final_loss, output_dir, + duration_seconds, error_message, loss_sparkline + FROM training_runs + ORDER BY started_at DESC + LIMIT ? OFFSET ? + """, + (limit, offset), + ).fetchall() + runs = [] + for row in rows: + run = dict(row) + sparkline = run.get("loss_sparkline") + if sparkline: + try: + run["loss_sparkline"] = json.loads(sparkline) + except (json.JSONDecodeError, TypeError): + logger.debug( + "Failed to parse loss_sparkline for run %s", run.get("id") + ) + run["loss_sparkline"] = None + runs.append(run) + return {"runs": runs, "total": total} + finally: + conn.close() + + +def get_run(id: str) -> Optional[dict]: + conn = get_connection() + try: + row = conn.execute("SELECT * FROM training_runs WHERE id = ?", (id,)).fetchone() + if row is None: + return None + run = dict(row) + sparkline = run.get("loss_sparkline") + if sparkline: + try: + run["loss_sparkline"] = json.loads(sparkline) + except (json.JSONDecodeError, TypeError): + logger.debug("Failed to parse loss_sparkline for run %s", id) + run["loss_sparkline"] = None + return run + finally: + conn.close() + + +def get_run_metrics(id: str) -> dict: + """Return metric arrays for a run, using paired step arrays per metric.""" + conn = get_connection() + try: + rows = conn.execute( + """ + SELECT step, loss, learning_rate, grad_norm, eval_loss, epoch, + num_tokens, elapsed_seconds + FROM training_metrics + WHERE run_id = ? + ORDER BY step + """, + (id,), + ).fetchall() + + step_history: list[int] = [] + loss_history: list[float] = [] + loss_step_history: list[int] = [] + lr_history: list[float] = [] + lr_step_history: list[int] = [] + grad_norm_history: list[float] = [] + grad_norm_step_history: list[int] = [] + eval_loss_history: list[float] = [] + eval_step_history: list[int] = [] + final_epoch: float | None = None + final_num_tokens: int | None = None + + for row in rows: + step = row["step"] + step_history.append(step) + if step > 0 and row["loss"] is not None: + loss_history.append(row["loss"]) + loss_step_history.append(step) + if step > 0 and row["learning_rate"] is not None: + lr_history.append(row["learning_rate"]) + lr_step_history.append(step) + if step > 0 and row["grad_norm"] is not None: + grad_norm_history.append(row["grad_norm"]) + grad_norm_step_history.append(step) + if step > 0 and row["eval_loss"] is not None: + eval_loss_history.append(row["eval_loss"]) + eval_step_history.append(step) + if row["epoch"] is not None: + final_epoch = row["epoch"] + if row["num_tokens"] is not None: + final_num_tokens = row["num_tokens"] + + return { + "step_history": step_history, + "loss_history": loss_history, + "loss_step_history": loss_step_history, + "lr_history": lr_history, + "lr_step_history": lr_step_history, + "grad_norm_history": grad_norm_history, + "grad_norm_step_history": grad_norm_step_history, + "eval_loss_history": eval_loss_history, + "eval_step_history": eval_step_history, + "final_epoch": final_epoch, + "final_num_tokens": final_num_tokens, + } + finally: + conn.close() + + +def delete_run(id: str) -> None: + conn = get_connection() + try: + conn.execute("DELETE FROM training_runs WHERE id = ?", (id,)) + conn.commit() + finally: + conn.close() + + +def cleanup_orphaned_runs() -> None: + """Mark any 'running' rows as errored on startup (server restarted mid-training).""" + from datetime import datetime, timezone + + conn = get_connection() + try: + conn.execute( + """ + UPDATE training_runs + SET status = 'error', + error_message = 'Server restarted during training', + ended_at = ? + WHERE status = 'running' + """, + (datetime.now(timezone.utc).isoformat(),), + ) + conn.commit() + finally: + conn.close() diff --git a/studio/backend/utils/downsample.py b/studio/backend/utils/downsample.py new file mode 100644 index 0000000000..bccf6a23b7 --- /dev/null +++ b/studio/backend/utils/downsample.py @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Generic numeric downsampling utility.""" + + +def downsample(values: list[float], target_count: int) -> list[float]: + """Reduce a list to target_count points via evenly-spaced index sampling.""" + if len(values) <= target_count: + return list(values) + if target_count <= 0: + return [] + if target_count == 1: + return [values[-1]] + indices = [ + round(i * (len(values) - 1) / (target_count - 1)) for i in range(target_count) + ] + return [values[i] for i in indices] diff --git a/studio/backend/utils/paths/__init__.py b/studio/backend/utils/paths/__init__.py index 90df216f96..789052f372 100644 --- a/studio/backend/utils/paths/__init__.py +++ b/studio/backend/utils/paths/__init__.py @@ -16,6 +16,7 @@ from .storage_roots import ( exports_root, auth_root, auth_db_path, + studio_db_path, tmp_root, seed_uploads_root, unstructured_seed_cache_root, @@ -45,6 +46,7 @@ __all__ = [ "exports_root", "auth_root", "auth_db_path", + "studio_db_path", "tmp_root", "seed_uploads_root", "unstructured_seed_cache_root", diff --git a/studio/backend/utils/paths/storage_roots.py b/studio/backend/utils/paths/storage_roots.py index f14887e119..626e868275 100644 --- a/studio/backend/utils/paths/storage_roots.py +++ b/studio/backend/utils/paths/storage_roots.py @@ -49,6 +49,10 @@ def auth_db_path() -> Path: return auth_root() / "auth.db" +def studio_db_path() -> Path: + return studio_root() / "studio.db" + + def tmp_root() -> Path: return Path(tempfile.gettempdir()) / "unsloth-studio" diff --git a/studio/frontend/src/features/studio/historical-training-view.tsx b/studio/frontend/src/features/studio/historical-training-view.tsx new file mode 100644 index 0000000000..d3ac434bd8 --- /dev/null +++ b/studio/frontend/src/features/studio/historical-training-view.tsx @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import type { TrainingViewData } from "@/features/training"; +import { getTrainingRun } from "@/features/training"; +import type { TrainingRunDetailResponse } from "@/features/training"; +import { type ReactElement, useEffect, useState } from "react"; +import { ChartsSection } from "./sections/charts-section"; +import { ProgressSection } from "./sections/progress-section"; + +interface HistoricalTrainingViewProps { + runId: string; +} + +function normalizeTrainingMethod(config: Record): string { + const type = config?.training_type as string | undefined; + if (!type || type === "Full Finetuning") return "full"; + if (type === "LoRA/QLoRA") { + return config?.load_in_4bit ? "qlora" : "lora"; + } + return "full"; +} + +function mapToViewData(detail: TrainingRunDetailResponse): TrainingViewData { + const { run, metrics } = detail; + + const lossHistory = metrics.loss_step_history + .map((step, i) => ({ step, value: metrics.loss_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const lrHistory = metrics.lr_step_history + .map((step, i) => ({ step, value: metrics.lr_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const gradNormHistory = metrics.grad_norm_step_history + .map((step, i) => ({ step, value: metrics.grad_norm_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const evalLossHistory = metrics.eval_step_history + .map((step, i) => ({ step, value: metrics.eval_loss_history[i] })) + .filter((p): p is { step: number; value: number } => p.value != null); + + const phase = + run.status === "completed" + ? "completed" + : run.status === "stopped" + ? "stopped" + : run.status === "error" + ? "error" + : run.status === "running" + ? "training" + : "idle"; + + return { + phase, + currentStep: run.final_step ?? 0, + totalSteps: run.total_steps ?? 0, + currentLoss: run.final_loss, + currentLearningRate: metrics.lr_history.at(-1) ?? null, + currentGradNorm: metrics.grad_norm_history.at(-1) ?? null, + currentEpoch: metrics.final_epoch, + currentNumTokens: metrics.final_num_tokens ?? null, + progressPercent: + run.total_steps && run.final_step + ? (run.final_step / run.total_steps) * 100 + : 0, + elapsedSeconds: run.duration_seconds, + etaSeconds: null, + evalEnabled: evalLossHistory.length > 0, + message: + run.status === "completed" + ? "Training completed" + : run.status === "stopped" + ? "Training stopped" + : run.status === "running" + ? "Training in progress" + : run.error_message ?? "Training errored", + error: run.status === "error" ? run.error_message : null, + isTrainingRunning: false, + modelName: run.model_name, + trainingMethod: normalizeTrainingMethod(detail.config), + lossHistory, + lrHistory, + gradNormHistory, + evalLossHistory, + }; +} + +export function HistoricalTrainingView({ + runId, +}: HistoricalTrainingViewProps): ReactElement { + const [detail, setDetail] = useState(null); + const [error, setError] = useState(null); + + // Derive loading from detail/error -- no separate state needed + const loading = detail === null && error === null; + + useEffect(() => { + const controller = new AbortController(); + getTrainingRun(runId, controller.signal) + .then((result) => { + setDetail(result); + }) + .catch((err) => { + if (err instanceof DOMException && err.name === "AbortError") return; + setError(err instanceof Error ? err.message : "Failed to load run"); + }); + return () => { + controller.abort(); + // Reset on runId change so loading derives correctly for the next fetch + setDetail(null); + setError(null); + }; + }, [runId]); + + if (loading) { + return ( +
+ Loading training run... +
+ ); + } + + if (error || !detail) { + return ( +
+ {error ?? "Run not found"} +
+ ); + } + + const viewData = mapToViewData(detail); + const configOverride = detail.config + ? { + epochs: detail.config.num_epochs as number | undefined, + batchSize: detail.config.batch_size as number | undefined, + learningRate: detail.config.learning_rate as string | undefined, + maxSteps: detail.config.max_steps as number | undefined, + contextLength: detail.config.max_seq_length as number | undefined, + warmupSteps: detail.config.warmup_steps as number | undefined, + optimizerType: detail.config.optim as string | undefined, + loraRank: detail.config.lora_r as number | undefined, + loraAlpha: detail.config.lora_alpha as number | undefined, + loraDropout: detail.config.lora_dropout as number | undefined, + loraVariant: detail.config.use_rslora ? "rsLoRA" : undefined, + } + : undefined; + + return ( +
+ + +
+ ); +} diff --git a/studio/frontend/src/features/studio/history-card-grid.tsx b/studio/frontend/src/features/studio/history-card-grid.tsx new file mode 100644 index 0000000000..78859d2f81 --- /dev/null +++ b/studio/frontend/src/features/studio/history-card-grid.tsx @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import type { TrainingRunSummary } from "@/features/training"; +import { deleteTrainingRun, listTrainingRuns } from "@/features/training"; +import { cn } from "@/lib/utils"; +import { Delete02Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { type ReactElement, useCallback, useEffect, useRef, useState } from "react"; +import { Spinner } from "@/components/ui/spinner"; + +const PAGE_SIZE = 12; +const RUNNING_POLL_INTERVAL_MS = 5000; + +const statusBadge: Record< + string, + { label: string; className: string } +> = { + completed: { + label: "Completed", + className: + "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400", + }, + stopped: { + label: "Stopped", + className: + "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-400", + }, + error: { + label: "Error", + className: "bg-red-100 text-red-700 dark:bg-red-950 dark:text-red-400", + }, + running: { + label: "Running", + className: + "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-400", + }, +}; + +function catmullRomPath(points: { x: number; y: number }[]): string { + if (points.length < 2) return ""; + const d = [`M${points[0]!.x.toFixed(1)},${points[0]!.y.toFixed(1)}`]; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[Math.max(i - 1, 0)]!; + const p1 = points[i]!; + const p2 = points[i + 1]!; + const p3 = points[Math.min(i + 2, points.length - 1)]!; + const cp1x = p1.x + (p2.x - p0.x) / 6; + const cp1y = p1.y + (p2.y - p0.y) / 6; + const cp2x = p2.x - (p3.x - p1.x) / 6; + const cp2y = p2.y - (p3.y - p1.y) / 6; + d.push( + `C${cp1x.toFixed(1)},${cp1y.toFixed(1)} ${cp2x.toFixed(1)},${cp2y.toFixed(1)} ${p2.x.toFixed(1)},${p2.y.toFixed(1)}`, + ); + } + return d.join(" "); +} + +function Sparkline({ values, id }: { values: number[]; id: string }): ReactElement | null { + if (!values || values.length < 2) return null; + let min = values[0]!; + let max = values[0]!; + for (let i = 1; i < values.length; i++) { + if (values[i]! < min) min = values[i]!; + if (values[i]! > max) max = values[i]!; + } + const range = max - min || 1; + const pad = 1.5; // half stroke-width so peaks aren't clipped + const h = 32; + const w = 120; + const gradientId = `sparkFill-${id}`; + + // Build points with vertical padding so the stroke isn't clipped + const pts = values.map((v, i) => ({ + x: (i / (values.length - 1)) * w, + y: pad + (1 - (v - min) / range) * (h - pad * 2), + })); + + const linePath = catmullRomPath(pts); + const last = pts[pts.length - 1]!; + const first = pts[0]!; + const fillPath = `${linePath} L${last.x.toFixed(1)},${h} L${first.x.toFixed(1)},${h} Z`; + + return ( + + + + + + + + + + + ); +} + +function formatRelativeTime(isoDate: string): string { + const diff = Date.now() - new Date(isoDate).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h ago`; + const days = Math.floor(hrs / 24); + return `${days}d ago`; +} + +function formatDuration(seconds: number | null): string { + if (seconds == null) return "--"; + const total = Math.floor(seconds); + if (total < 60) return `${total}s`; + const min = Math.floor(total / 60); + const sec = total % 60; + if (min < 60) return `${min}m ${sec}s`; + const hrs = Math.floor(min / 60); + return `${hrs}h ${min % 60}m`; +} + +interface HistoryCardGridProps { + onSelectRun: (runId: string) => void; +} + +export function HistoryCardGrid({ + onSelectRun, +}: HistoryCardGridProps): ReactElement { + const [runs, setRuns] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [deleteError, setDeleteError] = useState(null); + const [deleteTarget, setDeleteTarget] = useState(null); + const [manualFetchInFlight, setManualFetchInFlight] = useState(false); + + const userControllerRef = useRef(null); + const pollControllerRef = useRef(null); + const fetchIdRef = useRef(0); + const pollIdRef = useRef(0); + + const fetchRuns = useCallback(async (offset = 0, append = false, limit = PAGE_SIZE) => { + // Cancel any in-flight poll so its stale response can't clobber this fresher fetch + pollControllerRef.current?.abort(); + userControllerRef.current?.abort(); + const controller = new AbortController(); + userControllerRef.current = controller; + const id = ++fetchIdRef.current; + + setManualFetchInFlight(true); + setLoading(true); + setError(null); + try { + const result = await listTrainingRuns(limit, offset, controller.signal); + if (fetchIdRef.current !== id) return; + setRuns((prev) => (append ? [...prev, ...result.runs] : result.runs)); + setTotal(result.total); + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") return; + if (fetchIdRef.current !== id) return; + if (!append) setError("Failed to load training runs"); + } finally { + if (fetchIdRef.current === id) { + setLoading(false); + setManualFetchInFlight(false); + } + } + }, []); + + useEffect(() => { + void fetchRuns(0); + return () => { + userControllerRef.current?.abort(); + }; + }, [fetchRuns]); + + // Poll while any run is still "running" so the card shows live progress + const hasRunningRun = runs.some((r) => r.status === "running"); + const visibleCount = runs.length; + useEffect(() => { + if (!hasRunningRun) return; + const timer = setInterval(async () => { + if (manualFetchInFlight) return; + pollControllerRef.current?.abort(); + const controller = new AbortController(); + pollControllerRef.current = controller; + const pid = ++pollIdRef.current; + try { + const limit = Math.max(PAGE_SIZE, visibleCount); + const result = await listTrainingRuns(limit, 0, controller.signal); + if (pollIdRef.current !== pid) return; // stale poll — discard + setRuns(result.runs); + setTotal(result.total); + } catch { + // silently handle — poll will retry + } + }, RUNNING_POLL_INTERVAL_MS); + return () => { + clearInterval(timer); + pollControllerRef.current?.abort(); + }; + }, [hasRunningRun, visibleCount, manualFetchInFlight]); + + const handleDelete = async () => { + if (!deleteTarget) return; + setDeleteError(null); + try { + await deleteTrainingRun(deleteTarget); + // Optimistically remove the card so it disappears immediately + setRuns((prev) => prev.filter((r) => r.id !== deleteTarget)); + setTotal((prev) => Math.max(0, prev - 1)); + // Re-fetch preserving visible count so offsets stay consistent for "Load more" + const currentCount = runs.length - 1; + const limit = Math.max(PAGE_SIZE, currentCount); + fetchRuns(0, false, limit).catch(() => { + // Refresh failed — card is already removed, no stale display + }); + } catch { + setDeleteError("Failed to delete training run. Please try again."); + } + setDeleteTarget(null); + }; + + if (!loading && error && runs.length === 0) { + return ( +
+

{error}

+ +
+ ); + } + + if (!loading && runs.length === 0) { + return ( +
+

+ No training runs yet. Start your first training run in the Configure + tab. +

+
+ ); + } + + return ( + <> + {deleteError && ( +
+ {deleteError} +
+ )} +
+ {runs.map((run) => { + const badge = statusBadge[run.status] ?? statusBadge.error; + const isRunning = run.status === "running"; + return ( +
onSelectRun(run.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelectRun(run.id); + } + }} + > +
+ + {isRunning && } + {badge.label} + + + {formatRelativeTime(run.started_at)} + +
+
+

+ {run.model_name} +

+

+ {run.dataset_name} +

+
+ {run.loss_sparkline && run.loss_sparkline.length >= 2 && ( + + )} +
+ + Loss:{" "} + {run.final_loss != null ? run.final_loss.toFixed(4) : "--"} + + + Steps: {run.final_step ?? 0}/{run.total_steps ?? "--"} + + {formatDuration(run.duration_seconds)} +
+ {!isRunning && ( + + )} +
+ ); + })} +
+ {runs.length < total && ( +
+ +
+ )} + {loading && runs.length === 0 && ( +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ ))} +
+ )} + { + if (!open) setDeleteTarget(null); + }} + > + + + Delete training run? + + This will permanently delete this training run and all its metrics. + This action cannot be undone. + + + + Cancel + void handleDelete()} + > + Delete + + + + + + ); +} diff --git a/studio/frontend/src/features/studio/live-training-view.tsx b/studio/frontend/src/features/studio/live-training-view.tsx new file mode 100644 index 0000000000..9f930ce77b --- /dev/null +++ b/studio/frontend/src/features/studio/live-training-view.tsx @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { cn } from "@/lib/utils"; +import { + useTrainingConfigStore, + useTrainingRuntimeStore, +} from "@/features/training"; +import type { TrainingViewData } from "@/features/training"; +import type { ReactElement } from "react"; +import { useShallow } from "zustand/react/shallow"; +import { ChartsSection } from "./sections/charts-section"; +import { ProgressSection } from "./sections/progress-section"; +import { TrainingStartOverlay } from "./training-start-overlay"; + +export function LiveTrainingView(): ReactElement { + const runtime = useTrainingRuntimeStore( + useShallow((state) => ({ + jobId: state.jobId, + phase: state.phase, + message: state.message, + error: state.error, + currentStep: state.currentStep, + totalSteps: state.totalSteps, + currentEpoch: state.currentEpoch, + currentLoss: state.currentLoss, + currentLearningRate: state.currentLearningRate, + currentGradNorm: state.currentGradNorm, + currentNumTokens: state.currentNumTokens, + progressPercent: state.progressPercent, + elapsedSeconds: state.elapsedSeconds, + etaSeconds: state.etaSeconds, + evalEnabled: state.evalEnabled, + isTrainingRunning: state.isTrainingRunning, + lossHistory: state.lossHistory, + lrHistory: state.lrHistory, + gradNormHistory: state.gradNormHistory, + evalLossHistory: state.evalLossHistory, + firstStepReceived: state.firstStepReceived, + isStarting: state.isStarting, + })), + ); + + const config = useTrainingConfigStore( + useShallow((state) => ({ + selectedModel: state.selectedModel, + trainingMethod: state.trainingMethod, + })), + ); + + const viewData: TrainingViewData = { + phase: runtime.phase, + currentStep: runtime.currentStep, + totalSteps: runtime.totalSteps, + currentLoss: runtime.currentLoss, + currentLearningRate: runtime.currentLearningRate, + currentGradNorm: runtime.currentGradNorm, + currentEpoch: runtime.currentEpoch, + currentNumTokens: runtime.currentNumTokens, + progressPercent: runtime.progressPercent, + elapsedSeconds: runtime.elapsedSeconds, + etaSeconds: runtime.etaSeconds, + evalEnabled: runtime.evalEnabled, + message: runtime.message, + error: runtime.error, + isTrainingRunning: runtime.isTrainingRunning, + modelName: config.selectedModel ?? "", + trainingMethod: config.trainingMethod ?? "", + lossHistory: runtime.lossHistory, + lrHistory: runtime.lrHistory, + gradNormHistory: runtime.gradNormHistory, + evalLossHistory: runtime.evalLossHistory, + }; + + const isPreparingPhase = + runtime.phase === "downloading_model" || + runtime.phase === "downloading_dataset" || + runtime.phase === "loading_model" || + runtime.phase === "loading_dataset" || + runtime.phase === "configuring"; + const isWaitingForFirstStep = + runtime.phase === "training" && !runtime.firstStepReceived; + const showOverlay = + runtime.isStarting || + isPreparingPhase || + (isWaitingForFirstStep && runtime.currentStep <= 0); + + return ( +
+
+
+ +
+ +
+ {showOverlay ? ( + + ) : null} +
+ ); +} diff --git a/studio/frontend/src/features/studio/sections/charts-section.tsx b/studio/frontend/src/features/studio/sections/charts-section.tsx index a7dacf7e5b..4c7bf46d9d 100644 --- a/studio/frontend/src/features/studio/sections/charts-section.tsx +++ b/studio/frontend/src/features/studio/sections/charts-section.tsx @@ -1,7 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 -import { useTrainingRuntimeStore } from "@/features/training"; +import type { TrainingSeriesPoint } from "@/features/training"; import { type ReactElement, Suspense, lazy, useMemo } from "react"; const ChartsContent = lazy(() => @@ -16,42 +16,49 @@ const SKELETON_KEYS = [ "chart-skeleton-4", ]; -export function ChartsSection(): ReactElement | null { - const currentStep = useTrainingRuntimeStore((state) => state.currentStep); - const totalSteps = useTrainingRuntimeStore((state) => state.totalSteps); - const isTraining = useTrainingRuntimeStore((state) => state.isTrainingRunning); - const evalEnabled = useTrainingRuntimeStore((state) => state.evalEnabled); - const lossHistoryRaw = useTrainingRuntimeStore((state) => state.lossHistory); - const lrHistoryRaw = useTrainingRuntimeStore((state) => state.lrHistory); - const gradNormHistoryRaw = useTrainingRuntimeStore( - (state) => state.gradNormHistory, - ); - const evalLossHistoryRaw = useTrainingRuntimeStore( - (state) => state.evalLossHistory, - ); +interface ChartsSectionProps { + currentStep: number; + totalSteps: number; + isTraining: boolean; + evalEnabled: boolean; + lossHistory: TrainingSeriesPoint[]; + lrHistory: TrainingSeriesPoint[]; + gradNormHistory: TrainingSeriesPoint[]; + evalLossHistory: TrainingSeriesPoint[]; +} +export function ChartsSection({ + currentStep, + totalSteps, + isTraining, + evalEnabled, + lossHistory, + lrHistory, + gradNormHistory, + evalLossHistory, +}: ChartsSectionProps): ReactElement | null { const series = useMemo( () => ({ currentStep, totalSteps, - lossHistory: lossHistoryRaw.map((point) => ({ + lossHistory: lossHistory.map((point) => ({ step: point.step, loss: point.value, })), - lrHistory: lrHistoryRaw.map((point) => ({ + lrHistory: lrHistory.map((point) => ({ step: point.step, lr: point.value, })), - gradNormHistory: gradNormHistoryRaw.map((point) => ({ + gradNormHistory: gradNormHistory.map((point) => ({ step: point.step, gradNorm: point.value, })), - evalLossHistory: evalLossHistoryRaw.map((point) => ({ + evalLossHistory: evalLossHistory.map((point) => ({ step: point.step, loss: point.value, })), }), - [currentStep, evalLossHistoryRaw, gradNormHistoryRaw, lossHistoryRaw, lrHistoryRaw, totalSteps], + [currentStep, evalLossHistory, gradNormHistory, lossHistory, lrHistory, totalSteps], ); if ( diff --git a/studio/frontend/src/features/studio/sections/progress-section.tsx b/studio/frontend/src/features/studio/sections/progress-section.tsx index da89ce66ee..8255283183 100644 --- a/studio/frontend/src/features/studio/sections/progress-section.tsx +++ b/studio/frontend/src/features/studio/sections/progress-section.tsx @@ -26,6 +26,7 @@ import { useTrainingConfigStore, useTrainingRuntimeStore, } from "@/features/training"; +import type { TrainingViewData } from "@/features/training"; import { useGpuUtilization } from "@/hooks"; import { cn } from "@/lib/utils"; import { @@ -39,7 +40,7 @@ import { } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { Link, useNavigate } from "@tanstack/react-router"; -import { type ReactElement, type ReactNode, useEffect, useState } from "react"; +import { type ReactElement, type ReactNode, useState } from "react"; import { useShallow } from "zustand/react/shallow"; import { ChartSettingsSheet } from "./charts/chart-settings-sheet"; import { @@ -61,34 +62,33 @@ function configRow( return [label, value]; } -export function ProgressSection(): ReactElement { +interface ProgressSectionProps { + data: TrainingViewData; + isHistorical?: boolean; + configOverride?: { + epochs?: number; + batchSize?: number; + learningRate?: string; + maxSteps?: number; + contextLength?: number; + warmupSteps?: number; + optimizerType?: string; + loraRank?: number; + loraAlpha?: number; + loraDropout?: number; + loraVariant?: string; + }; +} + +export function ProgressSection({ + data, + isHistorical = false, + configOverride, +}: ProgressSectionProps): ReactElement { const navigate = useNavigate(); - const runtime = useTrainingRuntimeStore( - useShallow((state) => ({ - phase: state.phase, - message: state.message, - error: state.error, - currentStep: state.currentStep, - totalSteps: state.totalSteps, - currentEpoch: state.currentEpoch, - currentLoss: state.currentLoss, - currentLearningRate: state.currentLearningRate, - currentGradNorm: state.currentGradNorm, - progressPercent: state.progressPercent, - elapsedSeconds: state.elapsedSeconds, - etaSeconds: state.etaSeconds, - currentNumTokens: state.currentNumTokens, - isTrainingRunning: state.isTrainingRunning, - lossHistory: state.lossHistory, - lrHistory: state.lrHistory, - gradNormHistory: state.gradNormHistory, - })), - ); const config = useTrainingConfigStore( useShallow((state) => ({ - selectedModel: state.selectedModel, - trainingMethod: state.trainingMethod, epochs: state.epochs, batchSize: state.batchSize, learningRate: state.learningRate, @@ -103,98 +103,92 @@ export function ProgressSection(): ReactElement { })), ); - const { stopTrainingRun } = useTrainingActions(); - const gpu = useGpuUtilization(runtime.isTrainingRunning); const [stopDialogOpen, setStopDialogOpen] = useState(false); - const [stopRequested, setStopRequested] = useState(false); + const [stopRequestedLocal, setStopRequestedLocal] = useState(false); - useEffect(() => { - if (!runtime.isTrainingRunning) { - setStopRequested(false); - } - }, [runtime.isTrainingRunning]); + // Auto-reset when training stops -- no useEffect needed + const stopRequested = data.isTrainingRunning && stopRequestedLocal; const pct = - runtime.totalSteps > 0 + data.totalSteps > 0 ? Math.min( 100, Math.max( 0, - Math.round((runtime.currentStep / runtime.totalSteps) * 100), + Math.round((data.currentStep / data.totalSteps) * 100), ), ) - : Math.round(runtime.progressPercent); + : Math.round(data.progressPercent); - const elapsed = runtime.elapsedSeconds; + const elapsed = data.elapsedSeconds; const derivedEta = elapsed != null && pct > 0 ? Math.round((elapsed * (100 - pct)) / Math.max(pct, 1)) : null; - const eta = runtime.etaSeconds ?? derivedEta; + const eta = data.etaSeconds ?? derivedEta; const stepsPerSecond = - elapsed != null && elapsed > 0 ? runtime.currentStep / elapsed : null; + elapsed != null && elapsed > 0 ? data.currentStep / elapsed : null; const showHalfwayHint = - runtime.phase === "training" && pct >= 50 && pct < 100; - const showCompletedHint = runtime.phase === "completed"; + data.phase === "training" && pct >= 50 && pct < 100; + const showCompletedHint = data.phase === "completed"; const handleCompareInChat = async () => { - setTrainingCompareHandoff(config.selectedModel); + setTrainingCompareHandoff(data.modelName); await navigate({ to: "/chat" }); }; - const requestStop = async (saveCheckpoint: boolean) => { - setStopRequested(true); - setStopDialogOpen(false); - useTrainingRuntimeStore.getState().setStopRequested(true); - try { - const ok = await stopTrainingRun(saveCheckpoint); - if (!ok) { - setStopRequested(false); - } - } catch { - setStopRequested(false); - } - }; const stoppedLoss = getDisplayMetric( - runtime.isTrainingRunning, - runtime.currentLoss, - runtime.lossHistory, + data.isTrainingRunning, + data.currentLoss, + data.lossHistory, ); const stoppedLr = getDisplayMetric( - runtime.isTrainingRunning, - runtime.currentLearningRate, - runtime.lrHistory, + data.isTrainingRunning, + data.currentLearningRate, + data.lrHistory, ); - const stoppedGradNorm = runtime.isTrainingRunning - ? runtime.currentGradNorm - : (lastNonZeroValue(runtime.gradNormHistory) ?? runtime.currentGradNorm); + const stoppedGradNorm = data.isTrainingRunning + ? data.currentGradNorm + : (lastValue(data.gradNormHistory) ?? data.currentGradNorm); + + const cfgEpochs = isHistorical ? configOverride?.epochs : config.epochs; + const cfgBatchSize = isHistorical ? configOverride?.batchSize : config.batchSize; + const cfgLearningRate = isHistorical ? configOverride?.learningRate : config.learningRate; + const cfgMaxSteps = isHistorical ? configOverride?.maxSteps : config.maxSteps; + const cfgContextLength = isHistorical ? configOverride?.contextLength : config.contextLength; + const cfgWarmupSteps = isHistorical ? configOverride?.warmupSteps : config.warmupSteps; + const cfgOptimizerType = isHistorical ? configOverride?.optimizerType : config.optimizerType; + const cfgLoraRank = isHistorical ? configOverride?.loraRank : config.loraRank; + const cfgLoraAlpha = isHistorical ? configOverride?.loraAlpha : config.loraAlpha; + const cfgLoraDropout = isHistorical ? configOverride?.loraDropout : config.loraDropout; + const cfgLoraVariant = isHistorical ? configOverride?.loraVariant : config.loraVariant; const optimizerLabel = - OPTIMIZER_OPTIONS.find((o) => o.value === config.optimizerType)?.label ?? - config.optimizerType; + OPTIMIZER_OPTIONS.find((o) => o.value === cfgOptimizerType)?.label ?? + cfgOptimizerType; const configItems: ConfigGroup[] = [ { section: "Hyperparams", rows: [ - configRow("Epochs", config.epochs), - configRow("Batch size", config.batchSize), - configRow("Learning rate", config.learningRate), + configRow("Epochs", cfgEpochs), + configRow("Batch size", cfgBatchSize), + configRow("Learning rate", cfgLearningRate), configRow("Optimizer", optimizerLabel), - configRow("Max steps", config.maxSteps), - configRow("Context length", config.contextLength), - configRow("Warmup steps", config.warmupSteps), + configRow("Max steps", cfgMaxSteps), + configRow("Context length", cfgContextLength), + configRow("Warmup steps", cfgWarmupSteps), ], }, - ...(config.trainingMethod !== "full" + ...(data.trainingMethod !== "full" ? [ { section: "LoRA", rows: [ - configRow("Rank", config.loraRank), - configRow("Alpha", config.loraAlpha), - configRow("Dropout", config.loraDropout), - configRow("Variant", config.loraVariant), + configRow("Rank", cfgLoraRank), + configRow("Alpha", cfgLoraAlpha), + configRow("Dropout", cfgLoraDropout), + configRow("Variant", cfgLoraVariant), ], }, ] @@ -205,30 +199,34 @@ export function ProgressSection(): ReactElement { } title="Training Progress" - description={runtime.message || "Live training metrics"} + description={data.message || "Live training metrics"} accent="emerald" className="shadow-border border border-border/60 bg-card/90 ring-0 backdrop-blur-sm" headerAction={ - + isHistorical ? ( + + ) : ( + + ) } >
- {phaseLabel[runtime.phase]} + {phaseLabel[data.phase]} - Epoch {runtime.currentEpoch.toFixed(2)} + Epoch {formatNumber(data.currentEpoch, 2)} {pct}% complete @@ -238,22 +236,24 @@ export function ProgressSection(): ReactElement {
- Step {runtime.currentStep} / {runtime.totalSteps || "--"} + Step {data.currentStep} / {data.totalSteps || "--"} {pct}%
- + {!isHistorical && ( + + )} - {runtime.error && ( + {data.error && (

- {runtime.error} + {data.error}

)} @@ -262,97 +262,196 @@ export function ProgressSection(): ReactElement { label="Loss" valueClassName="text-2xl font-bold tracking-tight" > - {stoppedLoss.toFixed(4)} + {stoppedLoss != null ? stoppedLoss.toFixed(4) : "--"} - {stoppedLr.toExponential(2)} + {stoppedLr != null ? stoppedLr.toExponential(2) : "--"} {formatNumber(stoppedGradNorm, 3)} - {config.selectedModel ?? "--"} + {data.modelName || "--"} - {config.trainingMethod === "qlora" ? "QLoRA" : config.trainingMethod === "lora" ? "LoRA" : "Full"} + {data.trainingMethod === "qlora" ? "QLoRA" : data.trainingMethod === "lora" ? "LoRA" : "Full"}
Elapsed: {formatDuration(elapsed)} - ETA: {formatDuration(eta)} + {!isHistorical && ETA: {formatDuration(eta)}} {stepsPerSecond == null ? "-- steps/s" : `${stepsPerSecond.toFixed(2)} steps/s`} - {runtime.currentNumTokens != null && ( - Tokens: {runtime.currentNumTokens} + {data.currentNumTokens != null && ( + Tokens: {data.currentNumTokens} )}
-
-
-

- GPU Monitor -

- Live -
-
- - } - value={ - gpu.gpu_utilization_pct != null - ? `${gpu.gpu_utilization_pct}%` - : "--" - } - pct={gpu.gpu_utilization_pct ?? 0} - /> - - } - value={ - gpu.temperature_c != null ? `${gpu.temperature_c}°C` : "--" - } - pct={gpu.temperature_c ?? 0} - max={100} - /> - } - value={ - gpu.vram_used_gb != null && gpu.vram_total_gb != null - ? `${gpu.vram_used_gb} / ${gpu.vram_total_gb} GB` - : "--" - } - pct={gpu.vram_utilization_pct ?? 0} - /> - } - value={ - gpu.power_draw_w != null - ? gpu.power_limit_w != null - ? `${gpu.power_draw_w} / ${gpu.power_limit_w} W` - : `${gpu.power_draw_w} W` - : "--" - } - pct={gpu.power_utilization_pct ?? 0} - /> -
-
+ {!isHistorical && ( + + )}
); } +function LiveGpuPanel({ + isTrainingRunning, +}: { + isTrainingRunning: boolean; +}): ReactElement { + const gpu = useGpuUtilization(isTrainingRunning); + + return ( +
+
+

+ GPU Monitor +

+ Live +
+
+ + } + value={ + gpu.gpu_utilization_pct != null + ? `${gpu.gpu_utilization_pct}%` + : "--" + } + pct={gpu.gpu_utilization_pct ?? 0} + /> + + } + value={ + gpu.temperature_c != null ? `${gpu.temperature_c}°C` : "--" + } + pct={gpu.temperature_c ?? 0} + max={100} + /> + } + value={ + gpu.vram_used_gb != null && gpu.vram_total_gb != null + ? `${gpu.vram_used_gb} / ${gpu.vram_total_gb} GB` + : "--" + } + pct={gpu.vram_utilization_pct ?? 0} + /> + } + value={ + gpu.power_draw_w != null + ? gpu.power_limit_w != null + ? `${gpu.power_draw_w} / ${gpu.power_limit_w} W` + : `${gpu.power_draw_w} W` + : "--" + } + pct={gpu.power_utilization_pct ?? 0} + /> +
+
+ ); +} + +function LiveTrainingHeaderActions({ + configItems, + isTrainingRunning, + onOpenStopDialog, + stopDialogOpen, + stopRequested, + onSetStopRequested, +}: { + configItems: ConfigGroup[]; + isTrainingRunning: boolean; + onOpenStopDialog: (open: boolean) => void; + stopDialogOpen: boolean; + stopRequested: boolean; + onSetStopRequested: (v: boolean) => void; +}): ReactElement { + const { stopTrainingRun } = useTrainingActions(); + + const requestStop = async (saveCheckpoint: boolean) => { + onSetStopRequested(true); + onOpenStopDialog(false); + useTrainingRuntimeStore.getState().setStopRequested(true); + try { + const ok = await stopTrainingRun(saveCheckpoint); + if (!ok) { + onSetStopRequested(false); + } + } catch { + onSetStopRequested(false); + } + }; + + return ( + + ); +} + +function ConfigPopoverButton({ + configItems, +}: { + configItems: ConfigGroup[]; +}): ReactElement { + return ( + + + + + +
+

Training Config

+ {configItems.map((group) => ( +
+

+ {group.section} +

+ {group.rows.map(([label, value]) => ( +
+ {label} + + {value == null || value === "" ? "--" : String(value)} + +
+ ))} +
+ ))} +
+
+
+ ); +} + function TrainingHeaderActions({ configItems, isTrainingRunning, @@ -370,39 +469,7 @@ function TrainingHeaderActions({ }): ReactElement { return (
- - - - - -
-

Training Config

- {configItems.map((group) => ( -
-

- {group.section} -

- {group.rows.map(([label, value]) => ( -
- {label} - - {String(value)} - -
- ))} -
- ))} -
-
-
+ - )} -

Fine-tuning Studio

-

- {showTrainingView - ? runtimeMessage || "Training in progress" - : "Configure and start training"} -

+

{subtitle}

{!hasHydratedRuntime && isHydratingRuntime ? (
Loading training runtime...
- ) : showTrainingView ? ( - ) : ( -
- - - - -
+ +
+ {selectedHistoryRunId && activeTab === "history" && ( + + )} + + + Configure + + + Current Run + + History + +
+ + +
+ + + + +
+
+ + + + + + + {selectedHistoryRunId ? ( + + ) : ( + { + if (runId === currentJobId && isTrainingRunning) { + handleTabChange("current-run"); + } else { + setSelectedHistoryRunId(runId); + } + }} /> + )} + +
)}
diff --git a/studio/frontend/src/features/studio/training-view.tsx b/studio/frontend/src/features/studio/training-view.tsx deleted file mode 100644 index b995b2f528..0000000000 --- a/studio/frontend/src/features/studio/training-view.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 - -import { cn } from "@/lib/utils"; -import { useTrainingRuntimeStore } from "@/features/training"; -import type { ReactElement } from "react"; -import { useShallow } from "zustand/react/shallow"; -import { ChartsSection } from "./sections/charts-section"; -import { ProgressSection } from "./sections/progress-section"; -import { TrainingStartOverlay } from "./training-start-overlay"; - -export function TrainingView(): ReactElement { - const runtime = useTrainingRuntimeStore( - useShallow((state) => ({ - phase: state.phase, - message: state.message, - currentStep: state.currentStep, - firstStepReceived: state.firstStepReceived, - isStarting: state.isStarting, - })), - ); - - const isPreparingPhase = - runtime.phase === "downloading_model" || - runtime.phase === "downloading_dataset" || - runtime.phase === "loading_model" || - runtime.phase === "loading_dataset" || - runtime.phase === "configuring"; - const isWaitingForFirstStep = - runtime.phase === "training" && !runtime.firstStepReceived; - const showOverlay = - runtime.isStarting || - isPreparingPhase || - (isWaitingForFirstStep && runtime.currentStep <= 0); - - return ( -
-
-
- -
- -
- {showOverlay ? ( - - ) : null} -
- ); -} diff --git a/studio/frontend/src/features/training/api/history-api.ts b/studio/frontend/src/features/training/api/history-api.ts new file mode 100644 index 0000000000..8f279eb439 --- /dev/null +++ b/studio/frontend/src/features/training/api/history-api.ts @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +import { authFetch } from "@/features/auth"; +import type { + TrainingRunDeleteResponse, + TrainingRunDetailResponse, + TrainingRunListResponse, +} from "../types/history"; + +async function readError(response: Response): Promise { + try { + const payload = (await response.json()) as { detail?: string; message?: string }; + return payload.detail || payload.message || `Request failed (${response.status})`; + } catch { + return `Request failed (${response.status})`; + } +} + +async function parseJson(response: Response): Promise { + if (!response.ok) { + throw new Error(await readError(response)); + } + return (await response.json()) as T; +} + +export async function listTrainingRuns( + limit = 50, + offset = 0, + signal?: AbortSignal, +): Promise { + const response = await authFetch( + `/api/train/runs?limit=${limit}&offset=${offset}`, + { signal }, + ); + return parseJson(response); +} + +export async function getTrainingRun( + runId: string, + signal?: AbortSignal, +): Promise { + const response = await authFetch( + `/api/train/runs/${encodeURIComponent(runId)}`, + { signal }, + ); + return parseJson(response); +} + +export async function deleteTrainingRun( + runId: string, + signal?: AbortSignal, +): Promise { + const response = await authFetch( + `/api/train/runs/${encodeURIComponent(runId)}`, + { method: "DELETE", signal }, + ); + return parseJson(response); +} diff --git a/studio/frontend/src/features/training/index.ts b/studio/frontend/src/features/training/index.ts index 9a70e0a71c..af34b306cf 100644 --- a/studio/frontend/src/features/training/index.ts +++ b/studio/frontend/src/features/training/index.ts @@ -14,6 +14,14 @@ export { useDatasetPreviewDialogStore } from "./stores/dataset-preview-dialog-st export { uploadTrainingDataset } from "./api/datasets-api"; export { listLocalModels } from "./api/models-api"; export type { LocalModelInfo } from "./api/models-api"; -export type { TrainingPhase } from "./types/runtime"; +export type { TrainingPhase, TrainingViewData, TrainingSeriesPoint } from "./types/runtime"; +export type { + TrainingRunSummary, + TrainingRunListResponse, + TrainingRunMetrics, + TrainingRunDetailResponse, + TrainingRunDeleteResponse, +} from "./types/history"; +export { listTrainingRuns, getTrainingRun, deleteTrainingRun } from "./api/history-api"; export { parseYamlConfig, serializeConfigToYaml } from "./lib/yaml-config"; export { validateTrainingConfig } from "./lib/validation"; diff --git a/studio/frontend/src/features/training/types/history.ts b/studio/frontend/src/features/training/types/history.ts new file mode 100644 index 0000000000..8b89db539b --- /dev/null +++ b/studio/frontend/src/features/training/types/history.ts @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +export interface TrainingRunSummary { + id: string; + status: "running" | "completed" | "stopped" | "error"; + model_name: string; + dataset_name: string; + started_at: string; + ended_at: string | null; + total_steps: number | null; + final_step: number | null; + final_loss: number | null; + output_dir: string | null; + duration_seconds: number | null; + error_message: string | null; + loss_sparkline: number[] | null; +} + +export interface TrainingRunListResponse { + runs: TrainingRunSummary[]; + total: number; +} + +export interface TrainingRunMetrics { + step_history: number[]; + loss_history: number[]; + loss_step_history: number[]; + lr_history: number[]; + lr_step_history: number[]; + grad_norm_history: number[]; + grad_norm_step_history: number[]; + eval_loss_history: number[]; + eval_step_history: number[]; + final_epoch: number | null; + final_num_tokens: number | null; +} + +export interface TrainingRunDetailResponse { + run: TrainingRunSummary; + config: Record; + metrics: TrainingRunMetrics; +} + +export interface TrainingRunDeleteResponse { + status: string; + message: string; +} diff --git a/studio/frontend/src/features/training/types/runtime.ts b/studio/frontend/src/features/training/types/runtime.ts index 7669c8b2f3..1bf319a5d1 100644 --- a/studio/frontend/src/features/training/types/runtime.ts +++ b/studio/frontend/src/features/training/types/runtime.ts @@ -53,8 +53,8 @@ export interface TrainingProgressPayload { job_id: string; step: number; total_steps: number; - loss: number; - learning_rate: number; + loss: number | null; + learning_rate: number | null; progress_percent: number; epoch: number | null; elapsed_seconds: number | null; @@ -118,3 +118,32 @@ export interface TrainingRuntimeActions { } export type TrainingRuntimeStore = TrainingRuntimeState & TrainingRuntimeActions; + +export interface TrainingViewData { + // Current metrics (for ProgressSection) + phase: TrainingPhase; + currentStep: number; + totalSteps: number; + currentLoss: number | null; + currentLearningRate: number | null; + currentGradNorm: number | null; + currentEpoch: number | null; + currentNumTokens: number | null; + progressPercent: number; + elapsedSeconds: number | null; + etaSeconds: number | null; + evalEnabled: boolean; + message: string; + error: string | null; + isTrainingRunning: boolean; + + // Config summary + modelName: string; + trainingMethod: string; + + // Time-series (for ChartsSection) + lossHistory: TrainingSeriesPoint[]; + lrHistory: TrainingSeriesPoint[]; + gradNormHistory: TrainingSeriesPoint[]; + evalLossHistory: TrainingSeriesPoint[]; +} From 11606c502587d318ba21c7c2325d31e043f1cc0e Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Wed, 25 Mar 2026 01:00:12 -0700 Subject: [PATCH 09/34] fix: remove auto wandb.finish() after train() to allow post-training evaluate() (#4564) * fix: remove auto wandb.finish() after train() to allow post-training evaluate() The prepare_for_training_mode wrapper unconditionally called wandb.finish() after trainer.train() completed. This terminated the active W&B run, causing trainer.evaluate() to fail with "You must call wandb.init() before wandb.log()". Users who need multiple training runs in one session can call wandb.finish() manually between runs to avoid data overwriting. Fixes #3954 * fix: defer wandb.finish() to next train() call instead of removing it Instead of calling wandb.finish() at the end of train() (which breaks evaluate/log) or removing it entirely (which causes data overwriting on multiple train() calls), defer it to the start of the next train() call. This way: - train() + evaluate() works (run stays open after train) - train() + train() gets separate W&B runs (previous run finished first) - train() + evaluate() + train() also works correctly Also resets HF's WandbCallback._initialized flag so it re-calls wandb.init() for the new run. Fixes #3954 --------- Co-authored-by: Daniel Han --- unsloth/models/rl.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/unsloth/models/rl.py b/unsloth/models/rl.py index 52fd7f7ecc..581244e4d3 100755 --- a/unsloth/models/rl.py +++ b/unsloth/models/rl.py @@ -357,7 +357,6 @@ from transformers.training_args import ParallelMode from unsloth_zoo.device_type import DEVICE_TYPE, device_synchronize # Wrap trainer with padding to right and enable training mode -# Also patches W&B since multiple runs must use wandb.finish() import functools from types import MethodType try: @@ -367,6 +366,23 @@ except: def prepare_for_training_mode(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): + # Finish the previous W&B run if this is a subsequent train() call. + # We do this at the START of train() (not the end) so that + # evaluate() / log() still work after train() completes. + # HF's WandbCallback.setup() will call wandb.init() for the new run. + # See: https://github.com/unslothai/unsloth/issues/3954 + if getattr(self, '_unsloth_training_completed', False): + try: + import wandb + if wandb.run is not None: + wandb.finish() + # Reset HF's WandbCallback so it calls wandb.init() for the new run + for cb in self.callback_handler.callbacks: + if type(cb).__name__ == 'WandbCallback': + cb._initialized = False + break + except: + pass # Enable training mode _was_training = None # Get gradient checkpointing setting from training arguments @@ -387,12 +403,9 @@ def prepare_for_training_mode(f): reset_unsloth_gradient_checkpointing_buffers() except: pass - # Patch W&B to enable logging on future runs, otherwise it'll overwrite the first run - try: - import wandb - wandb.finish() - except: - pass + # Mark that training completed so the next train() call can + # finish this W&B run before starting a new one + self._unsloth_training_completed = True return output return wrapper pass From 45d0a343b5a46c7a711db40be6edc3381ead0733 Mon Sep 17 00:00:00 2001 From: Avaya Aggarwal <119044997+OnePunchMonk@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:33:10 +0530 Subject: [PATCH 10/34] =?UTF-8?q?feat:=20Implement=20Q-GaLore=20optimizer?= =?UTF-8?q?=20and=20custom=20embedding=20learning=20rate=E2=80=A6=20(#4511?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Implement Q-GaLore optimizer and custom embedding learning rate in the Unsloth trainer. * feat: Implement QGaLoreAdamW8bit optimizer with 8-bit states, GaLore low-rank gradient projection, and optional INT8 weight quantization, along with supporting projector and tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: Introduce Q-GaLore AdamW optimizer with low-rank quantized gradient projection and integrate into the trainer, along with dedicated tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: Implement Q-GaLore AdamW optimizer with gradient projection and quantization, including trainer integration and corresponding tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix 3 bugs in Q-GaLore optimizer and add weight_quant forward hooks 1. Fix use-after-delete crash: move `del p._saved_data` after the weight decay block so decoupled weight decay can reference the current weights correctly (p.data). 2. Fix substring matching in make_q_galore_param_groups: split parameter names on "." and check exact component matches to prevent false positives (e.g. "not_q_proj" matching "q_proj"). 3. Implement forward pre-hooks for weight_quant: after the optimizer quantizes weights to INT8, replace p.data with a 1-element placeholder to free float memory. A register_forward_pre_hook dequantizes back to float before each forward pass. The trainer calls install_weight_quant_hooks() when weight_quant is enabled. 4. Update test_weight_decay_uses_saved_data to match the fixed code path (decoupled decay uses p.data, expected value 2.7). Add test_weight_quant_hook_restores_float to verify the INT8-to-float hook round-trip. All 24/24 Q-GaLore tests pass. Benchmarked on Llama-3.2-1B-Instruct FFT: Q-GaLore saves 32% VRAM (10.63 -> 7.24 GB) with better loss convergence (1.3 vs 2.0 at step 100). No regressions in 31-notebook sweep across Llama, Qwen, Mistral, Phi, Gemma, vision, and GRPO. * Default weight_quant to False in QGaloreConfig Benchmarks show weight_quant=True adds ~1 GB on Llama-3.2-1B due to INT8 copy/scale overhead exceeding savings from the placeholder trick. Users can still opt in explicitly. The optimizer logic is unchanged. * Optimize Q-GaLore projector and optimizer step performance Projector (q_galore_projector.py): - Use torch.svd_lowrank with oversampling p=10 (Halko et al. 2009) instead of full SVD for large matrices. Falls back to full SVD when min(m,n) <= 2*rank. SVD steps are 6-8x faster on Llama-3.2-1B (22s -> 3s for first step). - Cache the dequantized ortho matrix between project() and project_back() to avoid redundant dequantization when quant=True. - Replace F.cosine_similarity with torch.dot for 1-D unit vectors in the adaptive schedule. Remove unused torch.nn.functional import. - Use collections.deque(maxlen=queue_size) instead of list with manual pop(0). Optimizer (q_galore_adamw.py): - Remove redundant .clone() on dequantized weights (line 151) and on float data before re-quantization (line 211). _dequantize already returns a fresh tensor and _quantize/_quantize_stochastic only reads its input. - Consolidate per-group torch.cuda.synchronize() into a single call after all param groups complete. - Use torch.empty instead of torch.zeros for the scalar placeholder tensor that is never read. Verified: 24/24 unit tests pass. Llama-3.2-1B 61-step training produces losses within 0.24% relative diff (correlation >0.9999) of the original. * [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 --- tests/utils/test_q_galore.py | 528 +++++++++++++++++++++++ unsloth/optimizers/__init__.py | 21 + unsloth/optimizers/q_galore_adamw.py | 424 ++++++++++++++++++ unsloth/optimizers/q_galore_projector.py | 385 +++++++++++++++++ unsloth/trainer.py | 142 +++++- 5 files changed, 1498 insertions(+), 2 deletions(-) create mode 100644 tests/utils/test_q_galore.py create mode 100644 unsloth/optimizers/__init__.py create mode 100644 unsloth/optimizers/q_galore_adamw.py create mode 100644 unsloth/optimizers/q_galore_projector.py diff --git a/tests/utils/test_q_galore.py b/tests/utils/test_q_galore.py new file mode 100644 index 0000000000..6dea5014a0 --- /dev/null +++ b/tests/utils/test_q_galore.py @@ -0,0 +1,528 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Tests for Q-GaLore integration (unsloth/optimizers/). + +import pytest +import sys +import os +import torch +import torch.nn as nn + +# Import the optimizers module directly to avoid triggering unsloth.__init__ +# which requires unsloth_zoo and other heavy dependencies. +_repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +_optimizers_dir = os.path.join(_repo_root, "unsloth", "optimizers") +if _repo_root not in sys.path: + sys.path.insert(0, _repo_root) + +# Direct import of the actual modules (avoids unsloth/__init__.py) +import importlib.util + + +def _load_module(name, filepath): + spec = importlib.util.spec_from_file_location(name, filepath) + mod = importlib.util.module_from_spec(spec) + sys.modules[name] = mod + spec.loader.exec_module(mod) + return mod + + +# Load projector module first (no dependencies on unsloth) +_projector_mod = _load_module( + "unsloth.optimizers.q_galore_projector", + os.path.join(_optimizers_dir, "q_galore_projector.py"), +) +GaLoreProjector = _projector_mod.GaLoreProjector +_quantize = _projector_mod._quantize +_dequantize = _projector_mod._dequantize +_quantize_stochastic = _projector_mod._quantize_stochastic + +# Load adamw module (depends on projector, may skip bitsandbytes) +_adamw_mod = _load_module( + "unsloth.optimizers.q_galore_adamw", + os.path.join(_optimizers_dir, "q_galore_adamw.py"), +) +make_q_galore_param_groups = _adamw_mod.make_q_galore_param_groups + +# ====================================================================== +# Projector tests +# ====================================================================== + + +class TestGaLoreProjector: + """Tests for the GaLore low-rank gradient projector.""" + + def test_project_and_back_tall(self): + """Project → project_back preserves shape for tall matrices.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1) + grad = torch.randn(16, 8) # tall + low = proj.project(grad, step = 0) + assert low.shape == (16, 4) + + full = proj.project_back(low) + assert full.shape == grad.shape + + def test_project_and_back_wide(self): + """Project → project_back preserves shape for wide matrices.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1) + grad = torch.randn(8, 16) # wide + low = proj.project(grad, step = 0) + assert low.shape == (4, 16) + + full = proj.project_back(low) + assert full.shape == grad.shape + + def test_project_reuses_cached_svd(self): + """SVD is not recomputed when step is not a multiple of update_proj_gap.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 100) + grad = torch.randn(16, 8) + proj.project(grad, step = 0) + assert proj.svd_count == 1 + + proj.project(grad, step = 1) + assert proj.svd_count == 1 # No recomputation + + proj.project(grad, step = 100) + assert proj.svd_count == 2 # Recomputed + + def test_quantized_projection(self): + """Quantized projection matrix stores and restores with bounded error.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, quant = True, n_bit = 8) + grad = torch.randn(16, 8) + low = proj.project(grad, step = 0) + assert low.shape == (16, 4) + + # The projection matrix should be stored as uint8 + assert proj.ortho_matrix.dtype == torch.uint8 + + def test_quantized_projection_int4(self): + """INT4 quantized projection stores correctly.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, quant = True, n_bit = 4) + grad = torch.randn(16, 8) + proj.project(grad, step = 0) + assert proj.ortho_matrix.dtype == torch.uint8 + # INT4 values should be in range [0, 15] + assert proj.ortho_matrix.max() <= 15 + + def test_adaptive_scheduling(self): + """update_proj_gap increases when cosine similarity exceeds threshold.""" + proj = GaLoreProjector( + rank = 4, + update_proj_gap = 10, + cos_threshold = 0.0, # Very low threshold → always triggers + gamma_proj = 2.0, + queue_size = 2, + ) + # Use very similar gradients so cosine similarity is high + base_grad = torch.randn(16, 8) + for i in range(5): + grad = base_grad + torch.randn_like(base_grad) * 0.001 + proj.project(grad, step = i * 10) + + # After several similar SVDs, update_proj_gap should have increased + assert proj.update_proj_gap > 10 + + def test_scale_applied(self): + """project_back applies the scale factor.""" + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, scale = 0.5) + grad = torch.randn(16, 8) + low = proj.project(grad, step = 0) + + proj2 = GaLoreProjector(rank = 4, update_proj_gap = 1, scale = 1.0) + low2 = proj2.project(grad, step = 0) + + full_half = proj.project_back(low) + full_one = proj2.project_back(low2) + + # The ratio should be exactly 0.5 (SVD is deterministic on same input) + ratio = full_half.norm() / full_one.norm() + assert abs(ratio - 0.5) < 1e-5, f"Expected ratio ~0.5, got {ratio:.8f}" + + +# ====================================================================== +# Quantization utility tests +# ====================================================================== + + +class TestQuantizationUtils: + """Tests for _quantize, _dequantize, _quantize_stochastic.""" + + def test_quantize_dequantize_roundtrip(self): + """Quantize → dequantize has bounded error.""" + w = torch.randn(32, 64) + q, scales, zeros, shape = _quantize(w, n_bit = 8) + w_hat = _dequantize(q, scales, zeros, shape) + + # Error should be bounded by the quantization step size + error = (w - w_hat).abs().max() + assert error < 0.1, f"Max error {error} exceeds threshold" + + def test_quantize_group_roundtrip(self): + """Grouped quantization → dequantization has bounded error.""" + w = torch.randn(32, 64) + q, scales, zeros, shape = _quantize(w, q_group_size = 32, n_bit = 8) + w_hat = _dequantize(q, scales, zeros, shape) + error = (w - w_hat).abs().max() + assert error < 0.1 + + def test_quantize_dtype(self): + """Quantized output should be uint8.""" + w = torch.randn(16, 16) + q, _, _, _ = _quantize(w, n_bit = 8) + assert q.dtype == torch.uint8 + + def test_quantize_int4_range(self): + """INT4 values should be in [0, 15].""" + w = torch.randn(16, 16) + q, _, _, _ = _quantize(w, n_bit = 4) + assert q.max() <= 15 + assert q.min() >= 0 + + def test_stochastic_rounding_unbiased(self): + """Stochastic rounding should be approximately unbiased.""" + torch.manual_seed(42) + w = torch.randn(64, 64) + errors = [] + for _ in range(50): + q, scales, zeros, shape = _quantize_stochastic(w, n_bit = 8) + w_hat = _dequantize(q, scales, zeros, shape) + errors.append((w - w_hat).mean().item()) + + mean_error = sum(errors) / len(errors) + assert ( + abs(mean_error) < 0.01 + ), f"Mean error {mean_error} suggests biased rounding" + + +# ====================================================================== +# Param group helper tests +# ====================================================================== + + +class TestParamGroupHelper: + """Tests for make_q_galore_param_groups.""" + + def test_param_group_separation(self): + """GaLore vs non-GaLore params are correctly separated.""" + + # Create a mini-transformer-like model + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + model.k_proj = nn.Linear(64, 64, bias = False) + model.embed = nn.Embedding(100, 64) + model.norm = nn.LayerNorm(64) + + groups = make_q_galore_param_groups(model, rank = 8, weight_quant = False) + + # Should have 2 groups: galore and non-galore + assert len(groups) == 2 + + galore_group = [g for g in groups if "rank" in g][0] + non_galore_group = [g for g in groups if "rank" not in g][0] + + # q_proj and k_proj should be in galore group (2 params) + assert len(galore_group["params"]) == 2 + # embed and norm should be in non-galore group + assert ( + len(non_galore_group["params"]) == 3 + ) # embed weight + norm weight + norm bias + + def test_custom_target_modules(self): + """Custom target_modules narrows GaLore scope.""" + + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + model.k_proj = nn.Linear(64, 64, bias = False) + model.v_proj = nn.Linear(64, 64, bias = False) + model.embed = nn.Embedding(100, 64) + + groups = make_q_galore_param_groups( + model, + rank = 8, + target_modules = ["q_proj"], + weight_quant = False, + ) + + galore_group = [g for g in groups if "rank" in g][0] + assert len(galore_group["params"]) == 1 # Only q_proj + + def test_bias_excluded_from_galore(self): + """1D bias params matching target names must NOT be in the GaLore group. + + GaLoreProjector.project requires 2-D gradients, so bias vectors + (e.g. q_proj.bias) that match a target name must be excluded. + """ + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = True) # has .weight AND .bias + model.embed = nn.Embedding(100, 64) + + groups = make_q_galore_param_groups(model, rank = 8, weight_quant = False) + + galore_group = [g for g in groups if "rank" in g][0] + non_galore_group = [g for g in groups if "rank" not in g][0] + + # Only the 2-D q_proj.weight should be in the GaLore group + assert len(galore_group["params"]) == 1 + assert galore_group["params"][0].dim() == 2 + + # q_proj.bias (1-D) + embed.weight should be in non-GaLore + assert any(p.dim() == 1 for p in non_galore_group["params"]) + + def test_empty_target_modules_no_galore(self): + """target_modules=[] should result in no GaLore params.""" + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + + # Pass empty list, should NOT fall back to defaults + groups = make_q_galore_param_groups( + model, + rank = 8, + target_modules = [], + weight_quant = False, + ) + + galore_groups = [g for g in groups if "rank" in g] + assert ( + len(galore_groups) == 0 + ), "Expected no GaLore groups when target_modules=[]" + + +# ====================================================================== +# Optimizer tests (CPU-only, no bitsandbytes dependency) +# ====================================================================== + + +class TestQGaLoreIntegration: + """Integration tests that work without bitsandbytes on CPU.""" + + def test_projector_training_loop(self): + """A simple training loop using manual GaLore projection converges.""" + torch.manual_seed(42) + + # Tiny model: single linear layer + model = nn.Linear(32, 16, bias = False) + target = torch.randn(4, 16) + x = torch.randn(4, 32) + + proj = GaLoreProjector(rank = 8, update_proj_gap = 1, scale = 1.0) + optimizer = torch.optim.AdamW(model.parameters(), lr = 0.01) + + losses = [] + for step in range(20): + optimizer.zero_grad() + out = model(x) + loss = nn.functional.mse_loss(out, target) + loss.backward() + losses.append(loss.item()) + + # Manual GaLore projection + for p in model.parameters(): + if p.grad is not None and p.grad.dim() == 2: + low = proj.project(p.grad, step) + p._saved = p.data.clone() + update = torch.zeros_like(low) + update.add_(low) # Simplified update + full_update = proj.project_back(update) + p.grad.copy_(full_update) + + optimizer.step() + + # Loss should decrease + assert ( + losses[-1] < losses[0] + ), f"Loss did not decrease: {losses[0]:.4f} → {losses[-1]:.4f}" + + def test_full_projector_roundtrip_quality(self): + """project → project_back captures the dominant gradient directions.""" + torch.manual_seed(42) + # Create a gradient with clear low-rank structure + u = torch.randn(32, 4) + v = torch.randn(4, 16) + grad = u @ v # rank-4 gradient + + proj = GaLoreProjector(rank = 4, update_proj_gap = 1, scale = 1.0) + low = proj.project(grad, step = 0) + reconstructed = proj.project_back(low) + + # For a rank-4 gradient with rank-4 projection, reconstruction + # should be very close to original + relative_error = (grad - reconstructed).norm() / grad.norm() + assert ( + relative_error < 0.05 + ), f"Reconstruction error too high: {relative_error:.4f}" + + def test_weight_quant_activates_on_first_step(self): + """_has_weight_quant returns True even when _q_scales is None (first step).""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + QGaLoreAdamW8bit = _adamw_mod_local.QGaLoreAdamW8bit + + p = torch.nn.Parameter(torch.randn(16, 16)) + # Simulate init_weight_quantization tagging + p._q_scales = None + p._q_zeros = None + p._q_shape = p.data.shape + + group = {"weight_quant": True} + + # _has_weight_quant must return True even on first step (_q_scales=None) + assert QGaLoreAdamW8bit._has_weight_quant(p, group) is True + + # Without the tag, it should return False + p2 = torch.nn.Parameter(torch.randn(16, 16)) + assert QGaLoreAdamW8bit._has_weight_quant(p2, group) is False + + def test_embedding_lr_param_group_split(self): + """Embedding params can be split into a separate group with custom LR.""" + # This tests the logic that make_q_galore_param_groups produces groups + # that can be further split by the trainer for embedding LR. + model = nn.Module() + model.q_proj = nn.Linear(64, 64, bias = False) + model.embed = nn.Embedding(100, 64) + + groups = make_q_galore_param_groups(model, rank = 8, weight_quant = False) + + # Simulate splitting non-GaLore group for embedding LR + embed_lr = 5e-5 + new_groups = [] + for group in groups: + if "rank" in group: + new_groups.append(group) + continue + embed_params = [] + other_params = [] + for p in group["params"]: + # In real usage, we'd check the name; here just split by shape + if p.shape[0] == 100: # embedding + embed_params.append(p) + else: + other_params.append(p) + if other_params: + g = dict(group) + g["params"] = other_params + new_groups.append(g) + if embed_params: + g = dict(group) + g["params"] = embed_params + g["lr"] = embed_lr + new_groups.append(g) + + # Should have 3 groups: galore, non-galore non-embed, embed + embed_groups = [g for g in new_groups if g.get("lr") == embed_lr] + assert len(embed_groups) == 1 + assert embed_groups[0]["lr"] == embed_lr + + def test_optimizer_hyperparams_forwarded(self): + """QGaLoreAdamW8bit accepts betas and eps keyword arguments.""" + # Verify the constructor signature accepts these params. + # Without bitsandbytes we can't instantiate, but we can check the + # function signature. + import inspect + + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + QGaLoreAdamW8bit = _adamw_mod_local.QGaLoreAdamW8bit + + sig = inspect.signature(QGaLoreAdamW8bit.__init__) + param_names = list(sig.parameters.keys()) + assert "betas" in param_names, "betas not in QGaLoreAdamW8bit.__init__ params" + assert "eps" in param_names, "eps not in QGaLoreAdamW8bit.__init__ params" + + def test_weight_decay_uses_saved_data(self): + """Weight decay should apply standard decoupled AdamW decay on current weights.""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + + # Create a mock parameter and group + p = torch.nn.Parameter(torch.ones(4, 4)) + p._saved_data = torch.ones(4, 4) * 2.0 # Pre-update weights + # Simulate project-back: p.data = p._saved_data + projected update + p.data = p._saved_data.add_(torch.ones(4, 4) * 1.0) # p.data is now 3.0 + + group = {"weight_decay": 0.1, "lr": 1.0, "_wd_saved": 0.1} + + # Replicate the fixed decoupled weight decay logic (uses p.data, not p._saved_data) + p.data.add_( + p.data, + alpha = -group["lr"] * group["_wd_saved"], + ) + + del p._saved_data # Clean up after all uses, matching fixed code + + # Decoupled weight decay: 3.0 - (1.0 * 0.1 * 3.0) = 2.7 + assert torch.allclose( + p.data, torch.tensor(2.7) + ), "Weight decay didn't use p.data for decoupled decay!" + + def test_params_float_after_weight_quant_step(self): + """After a step with weight_quant=True, parameters must remain floating point.""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + _projector_mod_local = sys.modules["unsloth.optimizers.q_galore_projector"] + + _quantize = _projector_mod_local._quantize + + p = torch.nn.Parameter(torch.randn(16, 16)) + group = { + "weight_quant": True, + "stochastic_round": False, + "weight_group_size": 16, + } + + # Replicate the re-quantize logic at the end of optimizer step + float_data = p.data.clone() + q, scales, zeros, shape = _quantize( + float_data, q_group_size = group["weight_group_size"] + ) + + # The key assertion: p.data stays float, _q_data holds uint8 + p._q_data = q.to(p.data.device) + p._q_scales = scales + p._q_zeros = zeros + p._q_shape = shape + + assert p.data.is_floating_point(), "p.data was converted to uint8!" + assert p._q_data.dtype == torch.uint8, "_q_data should be uint8!" + + def test_weight_quant_hook_restores_float(self): + """Forward pre-hook should dequantize INT8 weights before forward pass.""" + _adamw_mod_local = sys.modules["unsloth.optimizers.q_galore_adamw"] + _projector_mod_local = sys.modules["unsloth.optimizers.q_galore_projector"] + install_hook = _adamw_mod_local.install_weight_quant_hooks + + linear = nn.Linear(16, 8, bias = False) + original = linear.weight.data.clone() + + # Quantize the weight and replace with placeholder (simulates post-step) + q, scales, zeros, shape = _projector_mod_local._quantize( + linear.weight.data.clone(), q_group_size = 16 + ) + linear.weight._q_data = q + linear.weight._q_scales = scales + linear.weight._q_zeros = zeros + linear.weight._q_shape = shape + linear.weight.data = torch.zeros(1, dtype = linear.weight.dtype) + assert linear.weight.data.numel() == 1, "placeholder should be 1 element" + + # Install hook and run forward -- should restore float weights + handles = install_hook(linear) + x = torch.randn(2, 16) + out = linear(x) # triggers pre-hook + + assert linear.weight.data.shape == (8, 16), "weight shape not restored" + assert linear.weight.data.is_floating_point(), "weight not float after hook" + # Check values are close to original (quantization introduces small error) + assert torch.allclose( + linear.weight.data, original, atol = 0.15 + ), "dequantized weight too far from original" + + for h in handles: + h.remove() diff --git a/unsloth/optimizers/__init__.py b/unsloth/optimizers/__init__.py new file mode 100644 index 0000000000..b126321ab1 --- /dev/null +++ b/unsloth/optimizers/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .q_galore_projector import GaLoreProjector +from .q_galore_adamw import QGaLoreAdamW8bit + +__all__ = [ + "GaLoreProjector", + "QGaLoreAdamW8bit", +] diff --git a/unsloth/optimizers/q_galore_adamw.py b/unsloth/optimizers/q_galore_adamw.py new file mode 100644 index 0000000000..6cd0a4a846 --- /dev/null +++ b/unsloth/optimizers/q_galore_adamw.py @@ -0,0 +1,424 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Adapted from Q-GaLore (https://github.com/VITA-Group/Q-GaLore) +# Original paper: "Q-GaLore: Quantized GaLore with INT4 Projection and +# Layer-Adaptive Low-Rank Gradients" (arXiv:2407.08296) + +import torch +from typing import Optional, List + +from .q_galore_projector import ( + GaLoreProjector, + _quantize, + _quantize_stochastic, + _dequantize, +) + +__all__ = ["QGaLoreAdamW8bit", "install_weight_quant_hooks"] + +try: + import bitsandbytes.functional as bnb_F + from bitsandbytes.optim.optimizer import Optimizer2State + + _HAS_BNB = True +except ImportError: + _HAS_BNB = False + # Provide a fallback base so the module can at least be imported. + Optimizer2State = torch.optim.Optimizer + + +def _require_bnb(): + if not _HAS_BNB: + raise ImportError( + "Unsloth: Q-GaLore requires bitsandbytes. " + "Install it with: pip install bitsandbytes" + ) + + +class QGaLoreAdamW8bit(Optimizer2State): + """AdamW optimizer with 8-bit states, GaLore low-rank gradient projection, + and optional INT8 weight quantization. + + This optimizer combines three memory-saving techniques: + + 1. **8-bit optimizer states** (via bitsandbytes) — Adam's first and second + moments are stored in 8-bit, reducing optimizer state memory by ~4×. + + 2. **GaLore low-rank gradient projection** — gradients are projected into a + low-rank subspace before the optimizer step, then projected back. The + projection matrix itself can be quantized to INT4. + + 3. **INT8 weight quantization** — model weights are stored in INT8 during + training with stochastic rounding, reducing weight memory by ~2× for + eligible layers. + + Param group keys consumed by GaLore projection: + ``rank``, ``update_proj_gap``, ``scale``, ``proj_type``, + ``quant`` (projection quantization), ``quant_group_size``, + ``quant_n_bit``, ``cos_threshold``, ``gamma_proj``, ``queue_size`` + + Param group keys for weight quantization: + ``weight_quant``, ``stochastic_round``, ``weight_group_size`` + """ + + def __init__( + self, + params, + lr: float = 1e-3, + betas: tuple = (0.9, 0.999), + eps: float = 1e-8, + weight_decay: float = 1e-2, + min_8bit_size: int = 4096, + percentile_clipping: int = 100, + block_wise: bool = True, + is_paged: bool = False, + ): + _require_bnb() + super().__init__( + "adam", + params, + lr, + betas, + eps, + weight_decay, + 8, # optim_bits + None, # args + min_8bit_size, + percentile_clipping, + block_wise, + is_paged = is_paged, + ) + + # ------------------------------------------------------------------ + # Core step + # ------------------------------------------------------------------ + + @torch.no_grad() + def step(self, closure = None): + """Perform a single optimization step. + + For each parameter that has a ``rank`` key in its param group, the + following sequence is executed: + + 1. If ``weight_quant`` is set, dequantize the INT8 weight to float. + 2. Project the gradient to low-rank via the cached ``GaLoreProjector``. + 3. Perform the 8-bit Adam update in the low-rank space. + 4. Project the update back to full rank and add to saved weight. + 5. If ``weight_quant`` is set, re-quantize the weight to INT8. + """ + loss = None + if closure is not None: + with torch.enable_grad(): + loss = closure() + + if not self.initialized: + self.check_overrides() + self.to_gpu() + self.initialized = True + + for gindex, group in enumerate(self.param_groups): + for pindex, p in enumerate(group["params"]): + if p.grad is None: + continue + + state = self.state[p] + if "step" not in state: + state["step"] = 0 + + has_weight_quant = self._has_weight_quant(p, group) + + # --- Dequantize weight if INT8 --- + if has_weight_quant: + if p._q_scales is not None: + float_weight = _dequantize( + p._q_data, + p._q_scales, + p._q_zeros, + p._q_shape, + ) + p.data = float_weight + # else: first step, weights are still float — skip dequantize + + # --- GaLore projection --- + if "rank" in group: + if "projector" not in state: + state["projector"] = GaLoreProjector( + rank = group["rank"], + update_proj_gap = group.get("update_proj_gap", 200), + scale = group.get("scale", 0.25), + proj_type = group.get("proj_type", "std"), + quant = group.get("quant", False), + group_size = group.get("quant_group_size", -1), + n_bit = group.get("quant_n_bit", 4), + cos_threshold = group.get("cos_threshold", 0.4), + gamma_proj = group.get("gamma_proj", 2.0), + queue_size = group.get("queue_size", 5), + ) + + # Temporarily disable weight decay for GaLore params + # (we apply it manually after project-back) + if "weight_decay" in group and group["weight_decay"] > 0: + group["_wd_saved"] = group["weight_decay"] + group["weight_decay"] = 0 + + grad = state["projector"].project(p.grad, state["step"]) + + # Save current weight; replace p.data with zeros so + # the 8-bit update writes the pure weight delta. + p._saved_data = p.data.clone() + p.data = torch.zeros_like( + grad, dtype = p.data.dtype, device = p.data.device + ) + p.grad = grad + + # --- 8-bit Adam update --- + if "state1" not in state: + self.init_state(group, p, gindex, pindex) + + self.prefetch_state(p) + self.update_step(group, p, gindex, pindex) + + # --- GaLore project-back --- + if "rank" in group: + # p.data now holds the weight update in low-rank space + p.data = p._saved_data.add_(state["projector"].project_back(p.data)) + + # Re-apply decoupled weight decay using pre-update weights + if "_wd_saved" in group: + p.data.add_( + p.data, + alpha = -group["lr"] * group["_wd_saved"], + ) + group["weight_decay"] = group["_wd_saved"] + del group["_wd_saved"] + + del p._saved_data + + # --- Re-quantize weight to INT8 --- + if has_weight_quant: + float_data = p.data + stochastic = group.get("stochastic_round", True) + gsize = group.get("weight_group_size", 128) + quant_fn = _quantize_stochastic if stochastic else _quantize + q, scales, zeros, shape = quant_fn(float_data, q_group_size = gsize) + p._q_data = q.to(p.data.device) + p._q_scales = scales + p._q_zeros = zeros + p._q_shape = shape + # Replace p.data with a scalar placeholder to free float memory. + # A forward pre-hook (install_weight_quant_hooks) will + # dequantize back to float before the next forward pass. + p.data = torch.empty(1, dtype = p.data.dtype, device = p.data.device) + + state["step"] += 1 + + if torch.cuda.is_available(): + torch.cuda.synchronize() + + return loss + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _has_weight_quant(p: torch.Tensor, group: dict) -> bool: + """Check if this parameter uses INT8 weight quantization.""" + return ( + group.get("weight_quant", False) + and hasattr(p, "_q_scales") # tag set by init_weight_quantization() + ) + + @staticmethod + def init_weight_quantization( + model: torch.nn.Module, + param_groups: list, + group_size: int = 128, + stochastic: bool = True, + ) -> None: + """Tag parameters for INT8 weight quantization. + + This marks eligible weights with quantization metadata so that + the optimizer knows to quantize/dequantize them during ``step()``. + **Weights are NOT converted to uint8 here** — they remain in float + so that the first forward/backward pass runs correctly. The actual + quantization happens at the end of the first ``step()`` call. + """ + weight_quant_params = set() + for group in param_groups: + if group.get("weight_quant", False): + for p in group["params"]: + weight_quant_params.add(id(p)) + + for name, p in model.named_parameters(): + if id(p) in weight_quant_params: + # Store quantization metadata WITHOUT converting weights to + # uint8. The first optimizer.step() will quantize after the + # update. We store dummy scales/zeros so _has_weight_quant() + # returns True on the first step. + p._q_scales = None + p._q_zeros = None + p._q_shape = p.data.shape + p._stochastic_round = stochastic + p._weight_group_size = group_size + + +def _weight_quant_pre_hook(module, args): + """Forward pre-hook: dequantize INT8 weights to float before forward.""" + for p in module.parameters(recurse = False): + if hasattr(p, "_q_scales") and p._q_scales is not None: + float_weight = _dequantize( + p._q_data, + p._q_scales, + p._q_zeros, + p._q_shape, + ) + p.data = float_weight.to(p.data.device) + + +def install_weight_quant_hooks(model: torch.nn.Module) -> list: + """Register forward pre-hooks on modules whose weights are INT8-quantized. + + Returns a list of hook handles so the caller can remove them if needed. + """ + handles = [] + for module in model.modules(): + has_quant_param = any( + hasattr(p, "_q_scales") for p in module.parameters(recurse = False) + ) + if has_quant_param: + h = module.register_forward_pre_hook(_weight_quant_pre_hook) + handles.append(h) + return handles + + +# ====================================================================== +# Param-group construction helper +# ====================================================================== + +# Default linear layer names in transformer blocks that should use GaLore. +_DEFAULT_GALORE_TARGETS = { + "q_proj", + "k_proj", + "v_proj", + "o_proj", + "gate_proj", + "up_proj", + "down_proj", +} + + +def make_q_galore_param_groups( + model: torch.nn.Module, + lr: float = 1e-3, + weight_decay: float = 0.0, + rank: int = 256, + update_proj_gap: int = 200, + scale: float = 0.25, + proj_quant: bool = True, + proj_quant_group_size: int = -1, + proj_quant_n_bit: int = 4, + weight_quant: bool = False, + stochastic_round: bool = True, + weight_group_size: int = 128, + cos_threshold: float = 0.4, + gamma_proj: float = 2.0, + queue_size: int = 5, + target_modules: Optional[List[str]] = None, +) -> list: + """Build param groups suitable for :class:`QGaLoreAdamW8bit`. + + Parameters matching ``target_modules`` (or the default set of attention + and MLP projection names) are placed in the GaLore group. All other + trainable parameters go into the non-GaLore group. + + Args: + model: The model whose parameters to partition. + lr: Learning rate for all parameter groups. + weight_decay: Weight decay coefficient. + rank: GaLore projection rank. + update_proj_gap: Steps between SVD recomputations. + scale: Scaling factor for project-back. + proj_quant: Quantize projection matrices. + proj_quant_group_size: Group size for projection quantization. + proj_quant_n_bit: Bit-width for projection quantization. + weight_quant: Enable INT8 weight quantization for GaLore params. + stochastic_round: Use stochastic rounding for weight quantization. + weight_group_size: Group size for weight quantization. + cos_threshold: Cosine similarity threshold for adaptive scheduling. + gamma_proj: Multiplier for update_proj_gap when subspace is stable. + queue_size: Rolling window size for stability tracking. + target_modules: Module name substrings to match for GaLore. If None, + uses the default set of attention/MLP projection names. + + Returns: + List of two param group dicts: ``[galore_group, non_galore_group]``. + """ + targets = ( + set(target_modules) if target_modules is not None else _DEFAULT_GALORE_TARGETS + ) + + galore_params = [] + non_galore_params = [] + + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + + # Check if any target module name appears as a component in the param name. + # Exclude 1-D parameters (biases, norms) because GaLoreProjector.project + # requires 2-D gradients. + name_parts = name.split(".") + is_galore = param.dim() >= 2 and any(t in name_parts for t in targets) + + if is_galore: + galore_params.append(param) + else: + non_galore_params.append(param) + + groups = [] + + if galore_params: + groups.append( + { + "params": galore_params, + "lr": lr, + "weight_decay": weight_decay, + "rank": rank, + "update_proj_gap": update_proj_gap, + "scale": scale, + "proj_type": "std", + "quant": proj_quant, + "quant_group_size": proj_quant_group_size, + "quant_n_bit": proj_quant_n_bit, + "weight_quant": weight_quant, + "stochastic_round": stochastic_round, + "weight_group_size": weight_group_size, + "cos_threshold": cos_threshold, + "gamma_proj": gamma_proj, + "queue_size": queue_size, + } + ) + + if non_galore_params: + groups.append( + { + "params": non_galore_params, + "lr": lr, + "weight_decay": weight_decay, + } + ) + + return groups diff --git a/unsloth/optimizers/q_galore_projector.py b/unsloth/optimizers/q_galore_projector.py new file mode 100644 index 0000000000..cabd228b92 --- /dev/null +++ b/unsloth/optimizers/q_galore_projector.py @@ -0,0 +1,385 @@ +# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Adapted from Q-GaLore (https://github.com/VITA-Group/Q-GaLore) +# Original paper: "Q-GaLore: Quantized GaLore with INT4 Projection and +# Layer-Adaptive Low-Rank Gradients" (arXiv:2407.08296) + +from collections import deque + +import torch + +__all__ = ["GaLoreProjector"] + + +class GaLoreProjector: + """Low-rank gradient projector with optional INT4/INT8 quantized projection + matrices and layer-adaptive subspace update scheduling. + + The projector computes an SVD of the gradient to obtain an orthogonal basis + for the top-``rank`` subspace. Gradients are projected into this subspace + for the optimizer step, then projected back to full rank for the weight + update. + + Two key Q-GaLore innovations are implemented: + + 1. **Quantized projection matrices** — when ``quant=True``, the orthogonal + matrix is stored in INT4/INT8, reducing the memory cost of keeping the + projector state. + + 2. **Layer-adaptive update scheduling** — a rolling queue of cosine + similarities between consecutive orthogonal vectors is maintained. When + the average exceeds ``cos_threshold``, ``update_proj_gap`` is multiplied + by ``gamma_proj``, effectively reducing the frequency of expensive SVD + recomputations for layers whose subspace has stabilized. + + Args: + rank: Target rank for the low-rank projection. + update_proj_gap: Number of steps between SVD recomputations. + scale: Scaling factor applied when projecting back to full rank. + proj_type: Projection type. Only ``'std'`` is supported. + quant: Whether to quantize the projection matrix. + group_size: Group size for projection matrix quantization. + n_bit: Bit-width for projection matrix quantization (4 or 8). + cos_threshold: Cosine similarity threshold for adaptive scheduling. + gamma_proj: Multiplier for ``update_proj_gap`` on stability detection. + queue_size: Number of recent cosine similarities to average. + """ + + __slots__ = ( + "rank", + "update_proj_gap", + "scale", + "proj_type", + "quant", + "quant_group_size", + "quant_n_bit", + "cos_threshold", + "gamma_proj", + "queue_size", + "ortho_matrix", + "ortho_matrix_scales", + "ortho_matrix_zeros", + "ortho_matrix_shape", + "past_ortho_vector", + "queue", + "svd_count", + "_ortho_float_cache", + ) + + def __init__( + self, + rank: int, + update_proj_gap: int = 200, + scale: float = 1.0, + proj_type: str = "std", + quant: bool = False, + group_size: int = -1, + n_bit: int = 4, + cos_threshold: float = 0.4, + gamma_proj: float = 2.0, + queue_size: int = 5, + ): + self.rank = rank + self.update_proj_gap = update_proj_gap + self.scale = scale + self.proj_type = proj_type + + # Quantization settings for the projection matrix + self.quant = quant + self.quant_group_size = group_size + self.quant_n_bit = n_bit + + # Adaptive update scheduling state + self.cos_threshold = cos_threshold + self.gamma_proj = gamma_proj + self.queue_size = queue_size + self.past_ortho_vector = None + self.queue = deque(maxlen = queue_size) + self.svd_count = 0 + self._ortho_float_cache = None + + # Projection matrix state + self.ortho_matrix = None + self.ortho_matrix_scales = None + self.ortho_matrix_zeros = None + self.ortho_matrix_shape = None + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def project(self, full_rank_grad: torch.Tensor, step: int) -> torch.Tensor: + """Project a full-rank gradient into the low-rank subspace. + + The SVD is recomputed every ``update_proj_gap`` steps (subject to + adaptive scheduling). Between recomputations the cached orthogonal + matrix is reused. + + Args: + full_rank_grad: The full-rank gradient tensor (2-D). + step: The current optimizer step (0-indexed). + + Returns: + The low-rank gradient tensor. + """ + assert self.proj_type == "std", "Only proj_type='std' is supported." + + if full_rank_grad.shape[0] >= full_rank_grad.shape[1]: + # "tall" matrix → right projection (grad @ Q^T) + if self.ortho_matrix is None or step % self.update_proj_gap == 0: + float_ortho = self._compute_orthogonal( + full_rank_grad, + self.rank, + side = "right", + ) + self._update_adaptive_schedule(float_ortho, side = "right") + self._store_ortho(float_ortho) + + self._ortho_float_cache = self._load_ortho() + low_rank_grad = torch.matmul(full_rank_grad, self._ortho_float_cache.t()) + else: + # "wide" matrix → left projection (Q^T @ grad) + if self.ortho_matrix is None or step % self.update_proj_gap == 0: + float_ortho = self._compute_orthogonal( + full_rank_grad, + self.rank, + side = "left", + ) + self._update_adaptive_schedule(float_ortho, side = "left") + self._store_ortho(float_ortho) + + self._ortho_float_cache = self._load_ortho() + low_rank_grad = torch.matmul(self._ortho_float_cache.t(), full_rank_grad) + + return low_rank_grad + + def project_back(self, low_rank_grad: torch.Tensor) -> torch.Tensor: + """Project a low-rank update back to full rank. + + Args: + low_rank_grad: The low-rank gradient/update tensor. + + Returns: + The full-rank update scaled by ``self.scale``. + """ + float_ortho = self._ortho_float_cache + self._ortho_float_cache = None + if float_ortho is None: + float_ortho = self._load_ortho() + + if low_rank_grad.shape[0] >= low_rank_grad.shape[1]: + full_rank_grad = torch.matmul(low_rank_grad, float_ortho) + else: + full_rank_grad = torch.matmul(float_ortho, low_rank_grad) + + return full_rank_grad * self.scale + + # ------------------------------------------------------------------ + # SVD + # ------------------------------------------------------------------ + + @staticmethod + def _compute_orthogonal( + weights: torch.Tensor, + rank: int, + side: str, + ) -> torch.Tensor: + """Compute the top-``rank`` orthogonal matrix via truncated SVD. + + Args: + weights: 2-D tensor (typically the gradient). + rank: Number of singular vectors to keep. + side: ``'left'`` returns U[:, :rank], ``'right'`` returns Vh[:rank, :]. + + Returns: + Orthogonal matrix of shape ``(rank, N)`` (right) or ``(M, rank)`` (left). + """ + original_dtype = weights.dtype + original_device = weights.device + + matrix = weights.float() if original_dtype != torch.float32 else weights + + if side not in ("right", "left"): + raise ValueError(f"side must be 'left' or 'right', got '{side}'") + + m, n = matrix.shape + if min(m, n) <= rank * 2: + U, s, Vh = torch.linalg.svd(matrix, full_matrices = False) + result = Vh[:rank, :] if side == "right" else U[:, :rank] + else: + # Oversampling p=10 per Halko et al. 2009 (arXiv:0909.4061) + # recommendation of p=5..10 for large low-rank matrices. + q = min(rank + 10, min(m, n)) + U, s, V = torch.svd_lowrank(matrix, q = q, niter = 2) + result = V[:, :rank].t() if side == "right" else U[:, :rank] + + if original_dtype != torch.float32: + result = result.to(device = original_device, dtype = original_dtype) + return result + + # ------------------------------------------------------------------ + # Adaptive scheduling + # ------------------------------------------------------------------ + + def _update_adaptive_schedule( + self, + float_ortho: torch.Tensor, + side: str, + ) -> None: + """Track subspace stability and increase ``update_proj_gap`` if stable.""" + self.svd_count += 1 + + if side == "right": + current_vector = float_ortho[:1, :].flatten() + else: + current_vector = float_ortho[:, :1].flatten() + + if self.past_ortho_vector is not None: + cos_sim = torch.dot(self.past_ortho_vector, current_vector).item() + + self.queue.append(cos_sim) + + if ( + len(self.queue) == self.queue.maxlen + and sum(self.queue) / len(self.queue) >= self.cos_threshold + ): + self.update_proj_gap = int(self.update_proj_gap * self.gamma_proj) + + self.past_ortho_vector = current_vector.clone() + + # ------------------------------------------------------------------ + # Quantized projection matrix storage + # ------------------------------------------------------------------ + + def _store_ortho(self, float_ortho: torch.Tensor) -> None: + """Store the orthogonal matrix, optionally quantized.""" + if self.quant: + q, scales, zeros, shape = _quantize( + float_ortho, + q_group_size = self.quant_group_size, + n_bit = self.quant_n_bit, + ) + self.ortho_matrix = q + self.ortho_matrix_scales = scales + self.ortho_matrix_zeros = zeros + self.ortho_matrix_shape = shape + else: + self.ortho_matrix = float_ortho + + def _load_ortho(self) -> torch.Tensor: + """Load the orthogonal matrix, dequantizing if necessary.""" + if self.quant: + return _dequantize( + self.ortho_matrix, + self.ortho_matrix_scales, + self.ortho_matrix_zeros, + self.ortho_matrix_shape, + ) + return self.ortho_matrix + + +# ====================================================================== +# Quantization utilities (shared with the optimizer) +# ====================================================================== + + +@torch.no_grad() +def _quantize( + w: torch.Tensor, + q_group_size: int = -1, + n_bit: int = 8, +) -> tuple: + """Asymmetric min-max quantization to unsigned int. + + Returns: + ``(quantized_uint8, scales, zeros, original_shape)`` + """ + org_shape = w.shape + if q_group_size > 0: + assert ( + w.nelement() % q_group_size == 0 + ), f"Tensor size {w.nelement()} not divisible by group_size {q_group_size}" + w = w.reshape(-1, q_group_size) + assert w.dim() == 2 + + max_val = w.amax(dim = 1, keepdim = True) + min_val = w.amin(dim = 1, keepdim = True) + max_int = 2**n_bit - 1 + min_int = 0 + scales = (max_val - min_val).clamp(min = 1e-5) / max_int + zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int) + + w = torch.clamp(torch.round(w / scales) + zeros, min_int, max_int) + w = w.reshape(org_shape).to(torch.uint8) + + return w, scales, zeros, org_shape + + +@torch.no_grad() +def _dequantize( + w: torch.Tensor, + scales: torch.Tensor, + zeros: torch.Tensor, + original_shape: tuple, +) -> torch.Tensor: + """Dequantize from uint8 back to float.""" + # Infer group size: scales has shape (n_groups, 1), so n_groups = scales.shape[0] + total = w.numel() + n_groups = scales.shape[0] if scales.dim() > 1 else scales.numel() + group_size = total // n_groups if n_groups > 0 else total + + float_w = w.to(scales.dtype).reshape(-1, group_size) + float_w = (float_w - zeros) * scales + return float_w.reshape(original_shape) + + +@torch.no_grad() +def _quantize_stochastic( + w: torch.Tensor, + q_group_size: int = -1, + n_bit: int = 8, +) -> tuple: + """Asymmetric min-max quantization with stochastic rounding. + + Instead of deterministic ``round()``, the rounding direction is chosen + probabilistically proportional to the fractional part. This gives an + unbiased estimator of the original value in expectation. + + Returns: + ``(quantized_uint8, scales, zeros, original_shape)`` + """ + org_shape = w.shape + if q_group_size > 0: + assert w.nelement() % q_group_size == 0 + w = w.reshape(-1, q_group_size) + assert w.dim() == 2 + + max_val = w.amax(dim = 1, keepdim = True) + min_val = w.amin(dim = 1, keepdim = True) + max_int = 2**n_bit - 1 + min_int = 0 + scales = (max_val - min_val).clamp(min = 1e-5) / max_int + zeros = (-torch.round(min_val / scales)).clamp_(min_int, max_int) + + w_scaled = w / scales + up = torch.ceil(w_scaled) + down = torch.floor(w_scaled) + prob = w_scaled - down + rng = torch.rand_like(prob) + w = torch.where(rng < prob, up, down) + w = torch.clamp(w + zeros, min_int, max_int) + w = w.reshape(org_shape).to(torch.uint8) + + return w, scales, zeros, org_shape diff --git a/unsloth/trainer.py b/unsloth/trainer.py index 8bb4440021..eea985e958 100644 --- a/unsloth/trainer.py +++ b/unsloth/trainer.py @@ -17,7 +17,7 @@ import os import psutil import warnings from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, List from functools import wraps import trl @@ -46,6 +46,7 @@ __all__ = [ "unsloth_train", "_patch_trl_trainer", "UnslothVisionDataCollator", + "QGaloreConfig", ] logger = logging.getLogger(__name__) @@ -130,8 +131,39 @@ except: from transformers import TrainingArguments +@dataclass +class QGaloreConfig: + """Configuration for Q-GaLore optimizer integration. + + Pass an instance of this class to ``UnslothTrainingArguments`` (via + ``q_galore_config``) to enable Q-GaLore training. + """ + + rank: int = 256 + update_proj_gap: int = 200 + scale: float = 0.25 + proj_quant: bool = True + proj_quant_group_size: int = -1 + proj_quant_n_bit: int = 4 + weight_quant: bool = False + stochastic_round: bool = True + weight_group_size: int = 128 + cos_threshold: float = 0.4 + gamma_proj: float = 2.0 + queue_size: int = 5 + target_modules: Optional[List[str]] = None + + class UnslothTrainingArguments(TrainingArguments): - def __init__(self, embedding_learning_rate: float = None, *args, **kwargs): + def __init__( + self, + embedding_learning_rate: float = None, + q_galore_config: Optional[QGaloreConfig] = None, + *args, + **kwargs, + ): + self.q_galore_config = q_galore_config + self.embedding_learning_rate = embedding_learning_rate super().__init__(*args, **kwargs) self.embedding_learning_rate = embedding_learning_rate @@ -181,6 +213,13 @@ def _create_unsloth_optimizer( class UnslothTrainer(SFTTrainer): def create_optimizer(self): + # --- Q-GaLore optimizer --- + q_galore_config = getattr(self.args, "q_galore_config", None) + if q_galore_config is not None and self.optimizer is None: + embedding_lr = getattr(self.args, "embedding_learning_rate", None) + return self._create_q_galore_optimizer(q_galore_config, embedding_lr) + + # --- Embedding-LR optimizer --- embedding_learning_rate = getattr(self.args, "embedding_learning_rate", None) if embedding_learning_rate is None: return super().create_optimizer() @@ -197,6 +236,105 @@ class UnslothTrainer(SFTTrainer): ) return self.optimizer + def _create_q_galore_optimizer(self, config: "QGaloreConfig", embedding_lr = None): + """Build the Q-GaLore optimizer from a QGaloreConfig.""" + from unsloth.optimizers.q_galore_adamw import ( + QGaLoreAdamW8bit, + make_q_galore_param_groups, + install_weight_quant_hooks, + ) + + lr = self.args.learning_rate + weight_decay = self.args.weight_decay + + param_groups = make_q_galore_param_groups( + self.model, + lr = lr, + weight_decay = weight_decay, + rank = config.rank, + update_proj_gap = config.update_proj_gap, + scale = config.scale, + proj_quant = config.proj_quant, + proj_quant_group_size = config.proj_quant_group_size, + proj_quant_n_bit = config.proj_quant_n_bit, + weight_quant = config.weight_quant, + stochastic_round = config.stochastic_round, + weight_group_size = config.weight_group_size, + cos_threshold = config.cos_threshold, + gamma_proj = config.gamma_proj, + queue_size = config.queue_size, + target_modules = config.target_modules, + ) + + # --- Split embedding params with custom LR (Fix #2) --- + if embedding_lr is not None: + # Build a fast param->name lookup (O(N) instead of O(N*M)) + param_to_name = {id(p): name for name, p in self.model.named_parameters()} + + new_groups = [] + for group in param_groups: + if "rank" in group: + # GaLore group — keep as-is (embeddings are never in here) + new_groups.append(group) + continue + # Non-GaLore group: split out embedding params + embed_params = [] + other_params = [] + for p in group["params"]: + # Check if this param belongs to a modules_to_save embedding + name = param_to_name.get(id(p)) + if name and name.endswith("modules_to_save.default.weight"): + partial_name = name[: -len(".modules_to_save.default.weight")] + partial_name = partial_name[partial_name.rfind(".") + 1 :] + print( + f"Unsloth: Setting lr = {embedding_lr:.2e} instead of {lr:.2e} for {partial_name}." + ) + embed_params.append(p) + else: + other_params.append(p) + if other_params: + other_group = dict(group) + other_group["params"] = other_params + new_groups.append(other_group) + if embed_params: + embed_group = dict(group) + embed_group["params"] = embed_params + embed_group["lr"] = embedding_lr + new_groups.append(embed_group) + param_groups = new_groups + + # --- Forward optimizer hyperparameters (Fix #3) --- + self.optimizer = QGaLoreAdamW8bit( + param_groups, + lr = lr, + weight_decay = weight_decay, + betas = (self.args.adam_beta1, self.args.adam_beta2), + eps = self.args.adam_epsilon, + ) + + # Initialize INT8 weight quantization if enabled + if config.weight_quant: + QGaLoreAdamW8bit.init_weight_quantization( + self.model, + param_groups, + group_size = config.weight_group_size, + stochastic = config.stochastic_round, + ) + # Forward pre-hooks dequantize INT8 weights to float before each + # forward pass, allowing the optimizer to free float weight memory + # between steps. + install_weight_quant_hooks(self.model) + + n_galore = sum(len(g["params"]) for g in param_groups if "rank" in g) + n_other = sum(len(g["params"]) for g in param_groups if "rank" not in g) + print( + f"🦥 Unsloth: Q-GaLore enabled — " + f"{n_galore} GaLore params (rank={config.rank}), " + f"{n_other} standard params." + ) + + return self.optimizer + # From `trl>=0.13.0`, they changed how to pass several params to the trainer # We need to patch to make the transition smooth From 3998f67680ae292d599c6d8137dc8769c84ccea4 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 25 Mar 2026 02:01:43 -0700 Subject: [PATCH 11/34] Bump Data Designer to 0.5.4 (removes litellm dependency) (#4569) * Bump Data Designer to 0.5.4 (removes litellm dependency) NVIDIA Data Designer v0.5.4 removes litellm entirely and replaces it with native OpenAI and Anthropic adapters. This follows the litellm supply chain incident where versions 1.82.7 and 1.82.8 were compromised with a credential stealer. Release notes: https://github.com/NVIDIA-NeMo/DataDesigner/releases/tag/v0.5.4 Changes: - Bump data-designer, data-designer-config, data-designer-engine to 0.5.4 - Sync data-designer-deps.txt with 0.5.4 engine requirements: - Added: chardet, fsspec, mcp - Removed: python-json-logger, pymupdf, pymupdf4llm, mammoth (these remain in the unstructured-seed plugin which still needs them) - duckdb constraint relaxed from <1.5 to <2 (upstream fixed record_batch) - Bump plugin lower bound to >=0.5.4 * Keep pymupdf, pymupdf4llm, mammoth in data-designer-deps The unstructured-seed plugin is installed with --no-deps, so its pyproject.toml dependencies are not auto-resolved. These three packages are needed by the seed route (studio/backend/routes/ data_recipe/seed.py) and must remain in the explicit deps list. --- .../data-designer-unstructured-seed/pyproject.toml | 2 +- .../requirements/single-env/data-designer-deps.txt | 9 ++++++--- studio/backend/requirements/single-env/data-designer.txt | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml b/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml index d4770a0b05..c9d988bd9d 100644 --- a/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml +++ b/studio/backend/plugins/data-designer-unstructured-seed/pyproject.toml @@ -11,7 +11,7 @@ version = "0.1.0" description = "Local Data Designer unstructured seed reader plugin" requires-python = ">=3.11" dependencies = [ - "data-designer-engine>=0.5.1,<0.6", + "data-designer-engine>=0.5.4,<0.6", "pandas>=2,<3", "pymupdf>=1.24.0", "pymupdf4llm>=0.0.17", diff --git a/studio/backend/requirements/single-env/data-designer-deps.txt b/studio/backend/requirements/single-env/data-designer-deps.txt index 9cd0db99e4..0cb42db01d 100644 --- a/studio/backend/requirements/single-env/data-designer-deps.txt +++ b/studio/backend/requirements/single-env/data-designer-deps.txt @@ -1,8 +1,10 @@ # Data Designer runtime deps installed explicitly (single-env mode). -# DuckDB 1.5 removed Relation.record_batch(); keep <1.5 until upstream ships the fix. +# Synced with data-designer-engine==0.5.4 requirements. anyascii<1,>=0.3.3 -duckdb<1.5,>=1.1.3 +chardet<6,>=3.0.2 +duckdb<2,>=1.5.0 faker<21,>=20.1.0 +fsspec<2026,>=2025.3.0 httpx<1,>=0.27.2 httpx-retries<1,>=0.4.2 json-repair<1,>=0.48.0 @@ -10,12 +12,13 @@ jsonpath-rust-bindings<2,>=1.0 jsonschema<5,>=4.0.0 lxml<7,>=6.0.2 marko<3,>=2.1.2 +mcp<2,>=1.26.0 networkx<4,>=3.0 -python-json-logger<4,>=3 ruff<1,>=0.14.10 scipy<2,>=1.11.0 sqlfluff<4,>=3.2.0 tiktoken<1,>=0.8.0 +# Unstructured-seed plugin deps (plugin installed with --no-deps) pymupdf>=1.24.0 pymupdf4llm>=0.0.17 mammoth>=1.8.0 diff --git a/studio/backend/requirements/single-env/data-designer.txt b/studio/backend/requirements/single-env/data-designer.txt index 8daa1eca43..2e0ba62249 100644 --- a/studio/backend/requirements/single-env/data-designer.txt +++ b/studio/backend/requirements/single-env/data-designer.txt @@ -1,5 +1,5 @@ # Install Data Designer in same env as Unsloth. -data-designer==0.5.2 -data-designer-config==0.5.2 -data-designer-engine==0.5.2 +data-designer==0.5.4 +data-designer-config==0.5.4 +data-designer-engine==0.5.4 prompt-toolkit>=3,<4 From 926e74509d16a85d6ef8f149da5cc894dce096d0 Mon Sep 17 00:00:00 2001 From: Wasim Yousef Said Date: Wed, 25 Mar 2026 10:06:03 +0100 Subject: [PATCH 12/34] feat(chat): cleaner tool UI, inline LaTeX, clickable links (#4561) * feat(chat): ghost-style tool containers Remove borders and card styling from tool call UI. ToolFallback uses minimal padding with indented content. ToolGroup defaults to ghost variant with subtle background for multi-tool grouping. * feat(chat): compact web search source pills Switch sources from vertical full-width badges to horizontal wrapping pills with smaller icons. * feat(chat): left-accent code and terminal tool UI Replace bordered card layout with a left border accent for Python and Terminal tool output. Add timer cleanup on unmount for the copy button in both components. * feat(chat): inline latex and clickable links Enable single-dollar $...$ math rendering via createMathPlugin. Add styled link component with target=_blank for external links. * fix(chat): inline generating indicator, static tailwind classes, misc fixes Move generating indicator from viewport footer into assistant message using AnimatedShinyText shimmer. Only shows when message content is empty, hides once tool calls or text appear. Use static size class map in SourceIcon for Tailwind v4 compat. Use unique keys for web search sources. Remove px-3 from ghost tool group variant. * fix(chat): only show generating indicator while message is running Hide the shimmer when message is cancelled or errored with no content, preventing stale loading UI on empty completed messages. * fix: escape currency dollar signs in LaTeX math rendering and fix TS build error - Add preprocessLaTeX() in lib/latex.ts to escape currency patterns ($5, $1,000, $5.99, $100K) before they reach the math parser, preventing false positives when singleDollarTextMath is enabled. Code blocks and already-escaped dollars are left untouched. - Use preprocessLaTeX via useMemo in markdown-text.tsx so Streamdown receives clean input. - Fix TS18048 in thread.tsx: message.status?.type (optional chaining) since status can be undefined. --------- Co-authored-by: Daniel Han --- .../components/assistant-ui/markdown-text.tsx | 28 +++++- .../src/components/assistant-ui/sources.tsx | 3 +- .../src/components/assistant-ui/thread.tsx | 14 ++- .../components/assistant-ui/tool-fallback.tsx | 14 +-- .../components/assistant-ui/tool-group.tsx | 18 ++-- .../assistant-ui/tool-ui-python.tsx | 15 ++- .../assistant-ui/tool-ui-terminal.tsx | 13 ++- .../assistant-ui/tool-ui-web-search.tsx | 20 ++-- studio/frontend/src/lib/latex.ts | 97 +++++++++++++++++++ 9 files changed, 185 insertions(+), 37 deletions(-) create mode 100644 studio/frontend/src/lib/latex.ts diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 0bbbe94fdc..5e84b9175e 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -4,19 +4,39 @@ "use client"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; +import { preprocessLaTeX } from "@/lib/latex"; import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; import { code } from "@streamdown/code"; -import { math } from "@streamdown/math"; +import { createMathPlugin } from "@streamdown/math"; import { mermaid } from "@streamdown/mermaid"; import { DownloadIcon, Maximize2Icon, Minimize2Icon } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Block, type BlockProps, Streamdown } from "streamdown"; import "katex/dist/katex.min.css"; import { AudioPlayer } from "./audio-player"; +const math = createMathPlugin({ singleDollarTextMath: true }); const { withSmoothContextProvider } = INTERNAL; + +const STREAMDOWN_COMPONENTS = { + a: ({ + href, + children, + ...props + }: React.ComponentProps<"a">) => ( + + {children} + + ), +}; const COPY_RESET_MS = 2000; const MERMAID_SOURCE_RE = /```mermaid\s*([\s\S]*?)```/i; const CODE_FENCE_RE = /^```([^\r\n`]*)\r?\n([\s\S]*?)\r?\n?```$/; @@ -375,6 +395,7 @@ const AUDIO_PLAYER_RE = //; const MarkdownTextImpl = () => { const { text, status } = useMessagePartText(); + const processedText = useMemo(() => preprocessLaTeX(text), [text]); const audioMatch = text.match(AUDIO_PLAYER_RE); if (audioMatch) { @@ -387,6 +408,7 @@ const MarkdownTextImpl = () => { mode="streaming" isAnimating={status.type === "running"} plugins={{ code, math, mermaid }} + components={STREAMDOWN_COMPONENTS} controls={{ code: false, mermaid: { @@ -399,7 +421,7 @@ const MarkdownTextImpl = () => { shikiTheme={["github-light", "github-dark"]} BlockComponent={StreamdownBlock} > - {text} + {processedText}
); diff --git a/studio/frontend/src/components/assistant-ui/sources.tsx b/studio/frontend/src/components/assistant-ui/sources.tsx index 18ee03cad0..da97ff66b5 100644 --- a/studio/frontend/src/components/assistant-ui/sources.tsx +++ b/studio/frontend/src/components/assistant-ui/sources.tsx @@ -43,7 +43,8 @@ function SourceIcon({ }: ComponentProps<"span"> & { url: string; size?: number }) { const [hasError, setHasError] = useState(false); const domain = extractDomain(url); - const sizeClass = `size-${size}`; + const SIZE_CLASSES: Record = { 3: "size-3", 4: "size-4", 5: "size-5" }; + const sizeClass = SIZE_CLASSES[size] ?? "size-3"; if (hasError) { return ( diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 91cba5b02b..e5528f7ee0 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -16,6 +16,7 @@ import { WebSearchToolUI } from "@/components/assistant-ui/tool-ui-web-search"; import { PythonToolUI } from "@/components/assistant-ui/tool-ui-python"; import { TerminalToolUI } from "@/components/assistant-ui/tool-ui-terminal"; import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; +import { AnimatedShinyText } from "@/components/ui/animated-shiny-text"; import { Button } from "@/components/ui/button"; import { sentAudioNames } from "@/features/chat/api/chat-adapter"; import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils"; @@ -90,7 +91,6 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({ - !thread.isEmpty}> {!hideComposer && } @@ -541,6 +541,17 @@ const MessageError: FC = () => { ); }; +const GeneratingIndicator: FC = () => { + const show = useAuiState( + ({ message }) => + message.content.length === 0 && message.status?.type === "running", + ); + if (!show) return null; + return ( + Generating... + ); +}; + const AssistantMessage: FC = () => { return ( { data-role="assistant" >
+ -
{children}
+
{children}
); } @@ -226,7 +226,7 @@ function ToolFallbackArgs({ return (
@@ -251,7 +251,7 @@ function ToolFallbackResult({
     

@@ -316,7 +316,7 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({ return ( diff --git a/studio/frontend/src/components/assistant-ui/tool-group.tsx b/studio/frontend/src/components/assistant-ui/tool-group.tsx index f29adb510f..bf7a6a9a25 100644 --- a/studio/frontend/src/components/assistant-ui/tool-group.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-group.tsx @@ -26,11 +26,11 @@ const toolGroupVariants = cva("aui-tool-group-root group/tool-group w-full", { variants: { variant: { outline: "corner-squircle rounded-lg border py-3", - ghost: "", + ghost: "rounded-lg bg-muted/10 py-2", muted: "corner-squircle rounded-lg border border-muted-foreground/30 bg-muted/30 py-3", }, }, - defaultVariants: { variant: "outline" }, + defaultVariants: { variant: "ghost" }, }); export type ToolGroupRootProps = Omit< @@ -76,7 +76,7 @@ function ToolGroupRoot({ {label} @@ -189,6 +188,7 @@ function ToolGroupContent({ "mt-2 flex flex-col gap-2", "group-data-[variant=outline]/tool-group-root:mt-3 group-data-[variant=outline]/tool-group-root:border-t group-data-[variant=outline]/tool-group-root:px-4 group-data-[variant=outline]/tool-group-root:pt-3", "group-data-[variant=muted]/tool-group-root:mt-3 group-data-[variant=muted]/tool-group-root:border-t group-data-[variant=muted]/tool-group-root:px-4 group-data-[variant=muted]/tool-group-root:pt-3", + "group-data-[variant=ghost]/tool-group-root:mt-1 group-data-[variant=ghost]/tool-group-root:gap-1", )} > {children} 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 28468a10ad..a510ed0d9e 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-python.tsx @@ -7,7 +7,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { code as codePlugin } from "@streamdown/code"; import { CheckIcon, CodeIcon, CopyIcon, LoaderIcon } from "lucide-react"; -import { memo, useCallback, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Streamdown } from "streamdown"; import { ToolFallbackContent, @@ -28,6 +28,15 @@ function truncate(text: string): string { function CopyBtn({ text }: { text: string }) { const [copied, setCopied] = useState(false); const timer = useRef | null>(null); + + useEffect(() => { + return () => { + if (timer.current) { + clearTimeout(timer.current); + } + }; + }, []); + const copy = useCallback(() => { if (copyToClipboard(text)) { setCopied(true); @@ -98,14 +107,14 @@ const PythonToolUIImpl: ToolCallMessagePartComponent = ({ icon={CodeIcon} /> -

+
{/* Code + copy */} {code && (
)} - + {code && } {/* Output */} {isRunning ? ( diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx index 1b65b3c081..f233f951d3 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-terminal.tsx @@ -6,7 +6,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import type { ToolCallMessagePartComponent } from "@assistant-ui/react"; import { CheckIcon, CopyIcon, LoaderIcon, TerminalIcon } from "lucide-react"; -import { memo, useCallback, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { ToolFallbackContent, ToolFallbackRoot, @@ -25,6 +25,15 @@ function truncate(text: string): string { function CopyBtn({ text }: { text: string }) { const [copied, setCopied] = useState(false); const timer = useRef | null>(null); + + useEffect(() => { + return () => { + if (timer.current) { + clearTimeout(timer.current); + } + }; + }, []); + const copy = useCallback(() => { if (copyToClipboard(text)) { setCopied(true); @@ -74,7 +83,7 @@ const TerminalToolUIImpl: ToolCallMessagePartComponent = ({ icon={TerminalIcon} /> -
+
{isRunning ? (
diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx index 0635a83ec4..d3a86846de 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-web-search.tsx @@ -81,29 +81,27 @@ const WebSearchToolUIImpl: ToolCallMessagePartComponent = ({ /> {isRunning ? ( -
+
Searching for “{query}”…
) : sources.length > 0 ? ( -
- {sources.map((source) => ( +
+ {sources.map((source, i) => ( - - - {source.title} - + + {source.title} ))}
) : result ? ( -
+
               {typeof result === "string"
                 ? result
diff --git a/studio/frontend/src/lib/latex.ts b/studio/frontend/src/lib/latex.ts
new file mode 100644
index 0000000000..954d0f7bc6
--- /dev/null
+++ b/studio/frontend/src/lib/latex.ts
@@ -0,0 +1,97 @@
+// Adapted from LibreChat's latex.ts
+// https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts
+//
+// Escapes currency dollar signs so they are not misinterpreted as LaTeX math
+// delimiters when singleDollarTextMath is enabled.
+
+/**
+ * Matches a single $ followed by a number pattern (currency), e.g.:
+ *   $5, $1,000, $5.99, $100K, $3.5M
+ *
+ * Does NOT match:
+ *   $$ (display math), \$ (already escaped), $\alpha (LaTeX command)
+ */
+const CURRENCY_REGEX =
+  /(? {
+  const regions: Array<[number, number]> = [];
+
+  // Fenced code blocks: ```...```
+  const fencedRe = /```[\s\S]*?```/g;
+  let match: RegExpExecArray | null;
+  while ((match = fencedRe.exec(content)) !== null) {
+    regions.push([match.index, match.index + match[0].length]);
+  }
+
+  // Inline code: `...` (but not inside fenced blocks -- we filter below)
+  const inlineRe = /`[^`\n]+`/g;
+  while ((match = inlineRe.exec(content)) !== null) {
+    const start = match.index;
+    const end = start + match[0].length;
+    // Skip if this backtick span falls inside a fenced block
+    let inside = false;
+    for (const [rs, re] of regions) {
+      if (start >= rs && end <= re) {
+        inside = true;
+        break;
+      }
+    }
+    if (!inside) {
+      regions.push([start, end]);
+    }
+  }
+
+  // Sort by start position for binary search
+  regions.sort((a, b) => a[0] - b[0]);
+  return regions;
+}
+
+/**
+ * Binary search to check if a position falls inside any code region.
+ */
+function isInCodeBlock(
+  position: number,
+  regions: Array<[number, number]>,
+): boolean {
+  let lo = 0;
+  let hi = regions.length - 1;
+  while (lo <= hi) {
+    const mid = (lo + hi) >>> 1;
+    const [start, end] = regions[mid];
+    if (position < start) {
+      hi = mid - 1;
+    } else if (position >= end) {
+      lo = mid + 1;
+    } else {
+      return true;
+    }
+  }
+  return false;
+}
+
+/**
+ * Preprocess a markdown string to escape currency dollar signs so they are not
+ * parsed as LaTeX math delimiters.
+ *
+ * - `$5` alone becomes `\$5` (currency, not math)
+ * - `$\alpha$` is untouched (real LaTeX)
+ * - `$$E = mc^2$$` is untouched (display math)
+ * - Currency inside code blocks/spans is untouched
+ */
+export function preprocessLaTeX(content: string): string {
+  if (!content.includes("$")) return content;
+
+  const codeRegions = findCodeBlockRegions(content);
+
+  return content.replace(CURRENCY_REGEX, (match, offset) => {
+    if (isInCodeBlock(offset, codeRegions)) {
+      return match;
+    }
+    return "\\" + match;
+  });
+}

From 04359be333529298f6fd99ba3ac2cf57c79e8cfe Mon Sep 17 00:00:00 2001
From: Datta Nimmaturi 
Date: Wed, 25 Mar 2026 14:52:26 +0530
Subject: [PATCH 13/34] [Studio] Try installing causal-conv1d from prebuilt
 wheels if avialable (#4547)

* Try installing causal-conv1d from prebuilt wheels if avialable

* Prefer installing mamba-ssm from wheel to speed up things

* undo python stack install changes

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

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

* Revert "undo python stack install changes"

This reverts commit d943551092ea080355acdb70438c3a5d0083ea7f.

* add comments

* Fix wheel installer: model detection, platform tags, torch pin, error handling

- Add nemotron-h (hyphen) and granite-4.0-h / granitemoehybrid to model
  detection for both causal-conv1d and mamba-ssm. These hybrid Mamba models
  were silently skipped since nemotron_h (underscore) never matches real
  HF model IDs like nvidia/Nemotron-H-8B-Base, and granite was missing
  entirely despite being a supported model in model_config.py and loader.py.
- Fix _causal_conv1d_platform_tag to detect linux_aarch64 via
  platform.machine() instead of hardcoding linux_x86_64. Both upstream
  releases publish aarch64 wheels. Drop win_amd64 since neither repo
  publishes Windows wheels (avoids a wasted HTTP probe on every run).
- Pin torch to >=2.6.0,<2.11.0 instead of <=2.10.0 to add a version floor
  and document the wheel coverage range with upstream release links.
- Strip non-numeric suffixes from torch minor version so nightly builds
  like 2.7a0 correctly resolve to wheel tag torch2.7 instead of torch2.7a0.
- Use stderr=_sp.PIPE instead of stderr=_sp.STDOUT in the env probe so
  torch import warnings do not corrupt the JSON output.
- Add timeout=30 to the env probe subprocess to prevent indefinite hangs.
- Catch Exception (not just ImportError) on the existing-install check so
  ABI-broken installs with OSError/RuntimeError are retried rather than
  silently accepted.
- Guard uv invocation with shutil.which("uv") to prevent FileNotFoundError
  crash when uv is not on PATH. Wrap the top-level ensure calls in
  try/except so failures do not kill the training worker.
- Hoist _SSM_MODEL_SUBSTRINGS to module level.
- Remove redundant --torch-backend=auto flag from direct wheel URL install.

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

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

* Add LFM2 to causal-conv1d detection; stop training on install failure

- Add "lfm2" to _model_wants_causal_conv1d so Studio picks up the
  fast kernel path for Liquid Foundation Model 2.
- Replace silent logger.warning on SSM dependency install failure
  with an error event that tells the user to choose another model
  and stops the training job immediately.

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

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

* Catch subprocess timeout in torch probe; narrow import guard to ImportError

- _probe_causal_conv1d_env: wrap subprocess.run in try/except for
  TimeoutExpired so a slow torch import returns None (falls back to
  PyPI) instead of killing the training job.
- _install_package_wheel_first: narrow except Exception to except
  ImportError on the __import__ check so unexpected errors from a
  broken module still propagate.

* Remove unconditional torch pin from install_python_stack

The torch>=2.6.0,<2.11.0 pin was added to ensure prebuilt
causal-conv1d / mamba-ssm wheels exist, but it runs at install
time for all users regardless of model choice. This can downgrade
or unnecessarily upgrade torch. The worker already handles wheel
compatibility at training time by probing the environment and
falling back to PyPI, so the install-time pin is not needed.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han 
---
 studio/backend/core/training/worker.py | 336 ++++++++++++++++++++++---
 1 file changed, 297 insertions(+), 39 deletions(-)

diff --git a/studio/backend/core/training/worker.py b/studio/backend/core/training/worker.py
index d06dd6d358..891dfca8f7 100644
--- a/studio/backend/core/training/worker.py
+++ b/studio/backend/core/training/worker.py
@@ -16,15 +16,294 @@ from __future__ import annotations
 import structlog
 from loggers import get_logger
 import os
+import platform
+import shutil
 import sys
 import time
 import traceback
+import json
+import subprocess as _sp
 from pathlib import Path
 from typing import Any
+import urllib.error
+import urllib.request
 
 logger = get_logger(__name__)
 
 
+_CAUSAL_CONV1D_RELEASE_TAG = "v1.6.1.post4"
+_CAUSAL_CONV1D_PACKAGE_VERSION = "1.6.1"
+_MAMBA_SSM_RELEASE_TAG = "v2.3.1"
+_MAMBA_SSM_PACKAGE_VERSION = "2.3.1"
+
+
+def _model_wants_causal_conv1d(model_name: str) -> bool:
+    name = model_name.lower()
+    return any(
+        key in name
+        for key in (
+            "qwen3.5",
+            "qwen3_5",
+            "qwen3-next",
+            "qwen3_next",
+            "nemotron_h",
+            "nemotron-h",
+            "nemotron-3-nano",
+            "falcon_h1",
+            "falcon-h1",
+            "granite-4.0-h",
+            "granitemoehybrid",
+            "lfm2",
+        )
+    )
+
+
+def _causal_conv1d_platform_tag() -> str | None:
+    machine = platform.machine().lower()
+    if sys.platform.startswith("linux"):
+        if machine in {"x86_64", "amd64"}:
+            return "linux_x86_64"
+        if machine in {"aarch64", "arm64"}:
+            return "linux_aarch64"
+        return None
+    # No prebuilt wheels published for macOS or Windows
+    return None
+
+
+def _probe_causal_conv1d_env() -> dict[str, str] | None:
+    try:
+        probe = _sp.run(
+            [
+                sys.executable,
+                "-c",
+                (
+                    "import json, sys, re, torch; "
+                    "parts = torch.__version__.split('+', 1)[0].split('.')[:2]; "
+                    "minor = re.sub(r'[^0-9].*', '', parts[1]) if len(parts) > 1 else '0'; "
+                    "torch_mm = parts[0] + '.' + minor; "
+                    "print(json.dumps({"
+                    "'python_tag': f'cp{sys.version_info.major}{sys.version_info.minor}', "
+                    "'torch_mm': torch_mm, "
+                    "'cuda_major': str(int(str(torch.version.cuda).split('.', 1)[0])) if torch.version.cuda else '', "
+                    "'cxx11abi': str(torch._C._GLIBCXX_USE_CXX11_ABI).upper()"
+                    "}))"
+                ),
+            ],
+            stdout = _sp.PIPE,
+            stderr = _sp.PIPE,
+            text = True,
+            timeout = 30,
+        )
+    except _sp.TimeoutExpired:
+        logger.warning("Torch environment probe timed out after 30s")
+        return None
+    if probe.returncode != 0:
+        logger.warning(
+            "Failed to probe torch environment for causal-conv1d wheel:\n%s",
+            probe.stdout,
+        )
+        return None
+
+    try:
+        return json.loads(probe.stdout.strip())
+    except json.JSONDecodeError:
+        logger.warning(
+            "Failed to parse torch environment probe output: %s", probe.stdout
+        )
+        return None
+
+
+def _direct_wheel_url(
+    *,
+    filename_prefix: str,
+    package_version: str,
+    release_tag: str,
+    release_base_url: str,
+    env: dict[str, str] | None = None,
+) -> str | None:
+    env = env or _probe_causal_conv1d_env()
+    platform_tag = _causal_conv1d_platform_tag()
+    if env is None or platform_tag is None or not env.get("cuda_major"):
+        return None
+
+    filename = (
+        f"{filename_prefix}-{package_version}"
+        f"+cu{env['cuda_major']}torch{env['torch_mm']}"
+        f"cxx11abi{env['cxx11abi']}-{env['python_tag']}-{env['python_tag']}-{platform_tag}.whl"
+    )
+    return f"{release_base_url}/{release_tag}/{filename}"
+
+
+def _url_exists(url: str) -> bool:
+    try:
+        request = urllib.request.Request(url, method = "HEAD")
+        with urllib.request.urlopen(request, timeout = 10):
+            return True
+    except urllib.error.HTTPError as exc:
+        if exc.code == 404:
+            return False
+        logger.warning("Unexpected HTTP error while probing %s: %s", url, exc)
+        return False
+    except Exception as exc:
+        logger.warning("Failed to probe %s: %s", url, exc)
+        return False
+
+
+def _install_package_wheel_first(
+    *,
+    event_queue: Any,
+    import_name: str,
+    display_name: str,
+    pypi_name: str,
+    pypi_version: str,
+    filename_prefix: str,
+    release_tag: str,
+    release_base_url: str,
+) -> None:
+    try:
+        __import__(import_name)
+        logger.info("%s already installed", display_name)
+        return
+    except ImportError:
+        pass
+
+    env = _probe_causal_conv1d_env()
+    wheel_url = _direct_wheel_url(
+        filename_prefix = filename_prefix,
+        package_version = pypi_version,
+        release_tag = release_tag,
+        release_base_url = release_base_url,
+        env = env,
+    )
+
+    if wheel_url is None:
+        logger.info("No compatible %s wheel candidate", display_name)
+    else:
+        if _url_exists(wheel_url):
+            _send_status(event_queue, f"Installing prebuilt {display_name} wheel...")
+            installed = False
+            # Try uv first if available, then fall back to pip
+            if shutil.which("uv"):
+                uv_cmd = [
+                    "uv",
+                    "pip",
+                    "install",
+                    "--python",
+                    sys.executable,
+                    "--no-deps",
+                    wheel_url,
+                ]
+                result = _sp.run(
+                    uv_cmd,
+                    stdout = _sp.PIPE,
+                    stderr = _sp.STDOUT,
+                    text = True,
+                )
+                if result.returncode == 0:
+                    installed = True
+                else:
+                    logger.warning(
+                        "uv failed to install %s wheel:\n%s",
+                        display_name,
+                        result.stdout,
+                    )
+            if not installed:
+                pip_cmd = [
+                    sys.executable,
+                    "-m",
+                    "pip",
+                    "install",
+                    "--no-deps",
+                    wheel_url,
+                ]
+                result = _sp.run(
+                    pip_cmd,
+                    stdout = _sp.PIPE,
+                    stderr = _sp.STDOUT,
+                    text = True,
+                )
+                if result.returncode == 0:
+                    installed = True
+                else:
+                    logger.warning(
+                        "pip failed to install %s wheel:\n%s",
+                        display_name,
+                        result.stdout,
+                    )
+            if installed:
+                logger.info("Installed prebuilt %s wheel successfully", display_name)
+                return
+        else:
+            logger.info("No published %s wheel found: %s", display_name, wheel_url)
+
+    _send_status(event_queue, f"Installing {display_name} from PyPI...")
+    pypi_cmd = [
+        sys.executable,
+        "-m",
+        "pip",
+        "install",
+        "--no-build-isolation",
+        "--no-deps",
+        "--no-cache-dir",
+        f"{pypi_name}=={pypi_version}",
+    ]
+    result = _sp.run(
+        pypi_cmd,
+        stdout = _sp.PIPE,
+        stderr = _sp.STDOUT,
+        text = True,
+    )
+    if result.returncode != 0:
+        logger.error("Failed to install %s from PyPI:\n%s", display_name, result.stdout)
+        return
+
+    logger.info("Installed %s from PyPI", display_name)
+
+
+def _ensure_causal_conv1d_fast_path(event_queue: Any, model_name: str) -> None:
+    if not _model_wants_causal_conv1d(model_name):
+        return
+
+    _install_package_wheel_first(
+        event_queue = event_queue,
+        import_name = "causal_conv1d",
+        display_name = "causal-conv1d",
+        pypi_name = "causal-conv1d",
+        pypi_version = _CAUSAL_CONV1D_PACKAGE_VERSION,
+        filename_prefix = "causal_conv1d",
+        release_tag = _CAUSAL_CONV1D_RELEASE_TAG,
+        release_base_url = "https://github.com/Dao-AILab/causal-conv1d/releases/download",
+    )
+
+
+_SSM_MODEL_SUBSTRINGS = (
+    "nemotron_h",
+    "nemotron-h",
+    "nemotron-3-nano",
+    "falcon_h1",
+    "falcon-h1",
+    "granite-4.0-h",
+    "granitemoehybrid",
+)
+
+
+def _ensure_mamba_ssm(event_queue: Any, model_name: str) -> None:
+    if not any(sub in model_name.lower() for sub in _SSM_MODEL_SUBSTRINGS):
+        return
+
+    logger.info("SSM model detected; setting up mamba-ssm after causal-conv1d")
+    _install_package_wheel_first(
+        event_queue = event_queue,
+        import_name = "mamba_ssm",
+        display_name = "mamba-ssm",
+        pypi_name = "mamba-ssm",
+        pypi_version = _MAMBA_SSM_PACKAGE_VERSION,
+        filename_prefix = "mamba_ssm",
+        release_tag = _MAMBA_SSM_RELEASE_TAG,
+        release_base_url = "https://github.com/state-spaces/mamba/releases/download",
+    )
+
+
 def _activate_transformers_version(model_name: str) -> None:
     """Activate the correct transformers version BEFORE any ML imports.
 
@@ -121,45 +400,24 @@ def run_training_process(
             model_name,
         )
 
-    # ── 1b. Auto-install mamba-ssm for SSM/hybrid models (NemotronH, Falcon-H1) ──
-    _SSM_MODEL_SUBSTRINGS = ("nemotron_h", "nemotron-3-nano", "falcon_h1", "falcon-h1")
-    if any(sub in model_name.lower() for sub in _SSM_MODEL_SUBSTRINGS):
-        try:
-            import mamba_ssm  # noqa: F401
-
-            logger.info("mamba-ssm already installed")
-        except ImportError:
-            logger.info(
-                "SSM model detected — installing mamba-ssm and causal-conv1d (this may take several minutes)..."
-            )
-            _send_status(
-                event_queue, "Installing mamba-ssm (first time only, ~7 min)..."
-            )
-            import subprocess as _sp
-
-            # --no-build-isolation: compile against current torch (no version conflicts)
-            # --no-deps: don't pull in torch/transformers/triton (already installed)
-            for _pkg in ["causal_conv1d", "mamba_ssm"]:
-                _r = _sp.run(
-                    [
-                        sys.executable,
-                        "-m",
-                        "pip",
-                        "install",
-                        "--no-build-isolation",
-                        "--no-deps",
-                        "--no-cache-dir",
-                        _pkg,
-                    ],
-                    stdout = _sp.PIPE,
-                    stderr = _sp.STDOUT,
-                    text = True,
-                )
-                if _r.returncode != 0:
-                    logger.error("Failed to install %s:\n%s", _pkg, _r.stdout)
-                else:
-                    logger.info("Installed %s successfully", _pkg)
-            logger.info("mamba-ssm installation complete")
+    # ── 1b. Set up causal-conv1d first, then install mamba-ssm if needed ──
+    try:
+        _ensure_causal_conv1d_fast_path(event_queue, model_name)
+        _ensure_mamba_ssm(event_queue, model_name)
+    except Exception as exc:
+        event_queue.put(
+            {
+                "type": "error",
+                "error": (
+                    f"Please choose another model to train, since "
+                    f"causal-conv1d / mamba-ssm failed to install "
+                    f"with error: {exc}"
+                ),
+                "stack": traceback.format_exc(limit = 20),
+                "ts": time.time(),
+            }
+        )
+        return
 
     # ── 1c. Set fork start method so dataset.map() can multiprocess ──
     # The parent launched us via spawn (clean process), but the compiled

From efedbe97406e83892cb80f974bf22a5108223238 Mon Sep 17 00:00:00 2001
From: Pete Kloehn <35460307+pkloehn1@users.noreply.github.com>
Date: Wed, 25 Mar 2026 02:41:33 -0700
Subject: [PATCH 14/34] Feature/add dependabot and codeql security checks
 (#4479)

* Add CodeQL analysis workflow configuration

* Add Dependabot configuration for package updates

Configure Dependabot to check for updates in various ecosystems weekly.

* Fix dependabot.yml: bun ecosystem, missing dir, grouping for PR #4479

1. studio/frontend uses bun.lock not package-lock.json, so change npm to bun
2. Add missing studio/backend/requirements/ pip entry (consumed by studio/setup.sh)
3. Add groups with patterns ["*"] to all pip/bun/npm entries to batch updates
   and avoid 30+ individual Dependabot PRs on the first run

* Consolidate pip blocks to fix overlapping directory violation

GitHub Dependabot forbids multiple same-ecosystem entries with
overlapping directories on the same branch. The root "/" directory
overlapped the 3 nested pip dirs. Merge all 4 pip blocks into one
using the `directories:` (plural) key.

Also remove redundant open-pull-requests-limit from the bun block
since grouping with patterns: ["*"] already limits PR count.

---------

Co-authored-by: Daniel Han 
---
 .github/dependabot.yml       | 40 +++++++++++++++++++++++++++++++
 .github/workflows/codeql.yml | 46 ++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+)
 create mode 100644 .github/dependabot.yml
 create mode 100644 .github/workflows/codeql.yml

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000..a06cb1d114
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,40 @@
+---
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    groups:
+      actions:
+        patterns: ["*"]
+
+  - package-ecosystem: "pip"
+    directories:
+      - "/"
+      - "/studio/backend/plugins/data-designer-unstructured-seed"
+      - "/studio/backend/requirements"
+      - "/unsloth/kernels/moe"
+    schedule:
+      interval: "weekly"
+    open-pull-requests-limit: 10
+    groups:
+      pip:
+        patterns: ["*"]
+
+  - package-ecosystem: "bun"
+    directory: "/studio/frontend"
+    schedule:
+      interval: "weekly"
+    groups:
+      bun-frontend:
+        patterns: ["*"]
+
+  - package-ecosystem: "npm"
+    directory: "/studio/backend/core/data_recipe/oxc-validator"
+    schedule:
+      interval: "weekly"
+    groups:
+      npm-oxc-validator:
+        patterns: ["*"]
+...
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000000..b941b68857
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,46 @@
+---
+name: "CodeQL"
+
+on:
+  push:
+    branches: ["main"]
+  pull_request:
+    branches: ["main"]
+  schedule:
+    - cron: "25 14 * * 3"
+  workflow_dispatch:
+
+jobs:
+  analyze:
+    name: Analyze (${{ matrix.language }})
+    runs-on: ubuntu-latest
+    permissions:
+      security-events: write
+      packages: read
+      actions: read
+      contents: read
+
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+          - language: python
+            build-mode: none
+          - language: javascript-typescript
+            build-mode: none
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Initialize CodeQL
+        uses: github/codeql-action/init@v3
+        with:
+          languages: ${{ matrix.language }}
+          build-mode: ${{ matrix.build-mode }}
+
+      - name: Perform CodeQL Analysis
+        uses: github/codeql-action/analyze@v3
+        with:
+          category: "/language:${{ matrix.language }}"
+...

From f294161e26864512d0bd5acdbda84be6be884cfc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 02:44:22 -0700
Subject: [PATCH 15/34] build(deps): bump the actions group with 2 updates
 (#4570)

Bumps the actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `github/codeql-action` from 3 to 4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 .github/workflows/codeql.yml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index b941b68857..61c1b96d3b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -31,16 +31,16 @@ jobs:
 
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
 
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v3
+        uses: github/codeql-action/init@v4
         with:
           languages: ${{ matrix.language }}
           build-mode: ${{ matrix.build-mode }}
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v3
+        uses: github/codeql-action/analyze@v4
         with:
           category: "/language:${{ matrix.language }}"
 ...

From 38405cc18c4e20bc8fb2114ace04edb3b9c519c6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 02:44:38 -0700
Subject: [PATCH 16/34] build(deps): bump oxc-parser (#4571)

Bumps the npm-oxc-validator group in /studio/backend/core/data_recipe/oxc-validator with 1 update: [oxc-parser](https://github.com/oxc-project/oxc/tree/HEAD/napi/parser).


Updates `oxc-parser` from 0.116.0 to 0.121.0
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/napi/parser/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/crates_v0.121.0/napi/parser)

---
updated-dependencies:
- dependency-name: oxc-parser
  dependency-version: 0.121.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-oxc-validator
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 studio/backend/core/data_recipe/oxc-validator/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/studio/backend/core/data_recipe/oxc-validator/package.json b/studio/backend/core/data_recipe/oxc-validator/package.json
index a47c0ea521..d1c765a2e1 100644
--- a/studio/backend/core/data_recipe/oxc-validator/package.json
+++ b/studio/backend/core/data_recipe/oxc-validator/package.json
@@ -4,7 +4,7 @@
   "version": "0.0.1",
   "type": "module",
   "dependencies": {
-    "oxc-parser": "^0.116.0",
+    "oxc-parser": "^0.121.0",
     "oxlint": "^1.51.0"
   }
 }

From 6872c6e8505305e8fb6f69c0cf0e9ad4dbafd5d6 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 03:34:21 -0700
Subject: [PATCH 17/34] Remove advanced CodeQL workflow in favor of default
 setup (#4584)

The repo has both the CodeQL "default setup" (configured in repo
settings) and this advanced workflow file enabled. GitHub does not
allow both simultaneously, causing all PR CI runs to fail with:

  "CodeQL analyses from advanced configurations cannot be processed
   when the default setup is enabled"

Since the default setup already covers the same languages (Python,
JavaScript/TypeScript) with the same build-mode (none), remove the
redundant advanced workflow file.
---
 .github/workflows/codeql.yml | 46 ------------------------------------
 1 file changed, 46 deletions(-)
 delete mode 100644 .github/workflows/codeql.yml

diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index 61c1b96d3b..0000000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,46 +0,0 @@
----
-name: "CodeQL"
-
-on:
-  push:
-    branches: ["main"]
-  pull_request:
-    branches: ["main"]
-  schedule:
-    - cron: "25 14 * * 3"
-  workflow_dispatch:
-
-jobs:
-  analyze:
-    name: Analyze (${{ matrix.language }})
-    runs-on: ubuntu-latest
-    permissions:
-      security-events: write
-      packages: read
-      actions: read
-      contents: read
-
-    strategy:
-      fail-fast: false
-      matrix:
-        include:
-          - language: python
-            build-mode: none
-          - language: javascript-typescript
-            build-mode: none
-
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v6
-
-      - name: Initialize CodeQL
-        uses: github/codeql-action/init@v4
-        with:
-          languages: ${{ matrix.language }}
-          build-mode: ${{ matrix.build-mode }}
-
-      - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v4
-        with:
-          category: "/language:${{ matrix.language }}"
-...

From be2cd7087a8b083465866b60f8359a86eaa1bab7 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 03:37:37 -0700
Subject: [PATCH 18/34] Add macOS and Linux desktop shortcuts to install.sh
 (#4568)

* Add macOS and Linux desktop shortcuts to install.sh

Adds create_studio_shortcuts() function that creates platform-native
shortcuts after `unsloth studio setup` completes, mirroring the Windows
shortcut behavior from PR #4558.

Linux: .desktop file in ~/.local/share/applications/ and ~/Desktop/
macOS: .app bundle in ~/Applications/ with Info.plist, exec stub, and
       optional .icns icon built from unsloth-gem.png via sips+iconutil

Both platforms share a Bash launcher script at
~/.local/share/unsloth/launch-studio.sh that provides:
- Health check with service fingerprint verification
- Port scanning (8888-8908) via ss/lsof
- PID-file single-instance guard (no flock dependency)
- Terminal spawning (macOS: Terminal.app; Linux: gnome-terminal etc.)
- Browser open after health poll with 60s timeout

WSL is skipped (no native desktop environment).

* Fix 6 issues found by 10 parallel reviewers

1. [10/10] Health check now supports wget as fallback to curl via
   _http_get() helper, matching the installer's own download() pattern.
   Previously wget-only systems would time out on every launch.

2. [9/10] Exe path substitution now escapes sed metacharacters (&, \, |)
   and shell single-quotes before injection, preventing launcher
   corruption for paths like /opt/R&D/bin/unsloth.

3. [4/10] Linux .desktop Exec= field now quotes the launcher path,
   fixing launches from home directories containing spaces.

4. [3/10] macOS AppleScript command now escapes backslashes and
   double-quotes before interpolation into do script "...", fixing
   Terminal.app launch failures.

5. [3/10] Single-instance guard now uses atomic mkdir instead of
   racy check-then-write PID file, preventing duplicate concurrent
   launches on rapid double-click.

6. [1/10] Launcher now scans for a free port via _find_launch_port()
   instead of always hardcoding -p 8888, so Studio starts correctly
   when another service already occupies port 8888.

Also fixed: `open` command on Linux (openvt) no longer incorrectly
triggers the macOS browser-open path -- now gated on uname=Darwin.

* Fix mktemp guard and exe path escaping from PR review comments

Two real issues identified from automated review comments:

1. Guard mktemp -d failure in macOS icns generation. If mktemp -d
   returned empty, dirname would resolve to / and rm -rf would attempt
   to delete the root directory. Now checks that the temp dir was
   actually created before proceeding.

2. Replace sed-based exe path substitution with a conf file approach.
   The previous sed escaping broke paths containing apostrophes
   (e.g. /home/O'Connor/) because the '\'' escape introduced
   backslashes that were then double-escaped by the metacharacter
   pass. Now writes UNSLOTH_EXE to a separate studio.conf file that
   the launcher sources at runtime, eliminating all sed metacharacter
   and shell quoting interaction issues.

   This also addresses the sed -i.bak portability concern (now moot
   since sed is no longer used on the launcher file).

* Fix unbound variable crash and per-user lock in launcher

- Use ${UNSLOTH_EXE:-} so set -u does not crash before the friendly
  error message when studio.conf is missing or empty.
- Append $(id -u) to the fallback lock path so each user gets their
  own lock directory when XDG_RUNTIME_DIR is unset.

* Mark desktop shortcut as trusted for GNOME/Nautilus

On modern GNOME desktops, chmod +x alone is not sufficient to make
a .desktop file launchable by double-click on ~/Desktop. Nautilus
requires the metadata::trusted attribute to be set via gio, otherwise
it shows a warning dialog instead of launching the application.
---
 install.sh | 437 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 437 insertions(+)

diff --git a/install.sh b/install.sh
index 3d8e08612c..0893955939 100755
--- a/install.sh
+++ b/install.sh
@@ -89,6 +89,441 @@ _smart_apt_install() {
     fi
 }
 
+# ── Helper: create desktop shortcuts and launcher script ──
+# Usage: create_studio_shortcuts  
+# Creates ~/.local/share/unsloth/launch-studio.sh (shared launcher),
+# plus platform-specific shortcuts (Linux .desktop / macOS .app bundle).
+# Skipped on WSL (no native desktop).
+create_studio_shortcuts() {
+    _css_exe="$1"
+    _css_os="$2"
+
+    # Skip on WSL -- no native desktop environment
+    if [ "$_css_os" = "wsl" ]; then
+        return 0
+    fi
+
+    # Validate exe
+    if [ ! -x "$_css_exe" ]; then
+        echo "[WARN] Cannot create shortcuts: unsloth not found at $_css_exe"
+        return 0
+    fi
+
+    # Resolve absolute path
+    _css_exe_dir=$(cd "$(dirname "$_css_exe")" && pwd)
+    _css_exe="$_css_exe_dir/$(basename "$_css_exe")"
+
+    _css_data_dir="$HOME/.local/share/unsloth"
+    _css_launcher="$_css_data_dir/launch-studio.sh"
+    _css_icon_png="$_css_data_dir/unsloth-studio.png"
+    _css_gem_png="$_css_data_dir/unsloth-gem.png"
+
+    mkdir -p "$_css_data_dir"
+
+    # ── Write launcher script ──
+    # The launcher is Bash (not POSIX sh).
+    # We write it with a placeholder and substitute the exe path via sed.
+    cat > "$_css_launcher" << 'LAUNCHER_EOF'
+#!/usr/bin/env bash
+# Unsloth Studio Launcher
+# Auto-generated by install.sh -- do not edit manually.
+set -euo pipefail
+
+DATA_DIR="$HOME/.local/share/unsloth"
+
+# Read exe path from config written at install time.
+# Sourcing is safe: the config file is written by install.sh, not user input.
+if [ -f "$DATA_DIR/studio.conf" ]; then
+    . "$DATA_DIR/studio.conf"
+fi
+if [ -z "${UNSLOTH_EXE:-}" ] || [ ! -x "${UNSLOTH_EXE:-}" ]; then
+    echo "Error: UNSLOTH_EXE not set or not executable. Re-run the installer." >&2
+    exit 1
+fi
+
+BASE_PORT=8888
+MAX_PORT_OFFSET=20
+TIMEOUT_SEC=60
+POLL_INTERVAL_SEC=1
+LOG_FILE="$DATA_DIR/studio.log"
+LOCK_DIR="${XDG_RUNTIME_DIR:-/tmp}/unsloth-studio-launcher-$(id -u).lock"
+
+# ── HTTP GET helper (supports curl and wget) ──
+_http_get() {
+    _url="$1"
+    if command -v curl >/dev/null 2>&1; then
+        curl -fsS --max-time 1 "$_url" 2>/dev/null
+    elif command -v wget >/dev/null 2>&1; then
+        wget -qO- --timeout=1 "$_url" 2>/dev/null
+    else
+        return 1
+    fi
+}
+
+# ── Health check ──
+_check_health() {
+    _port=$1
+    _resp=$(_http_get "http://127.0.0.1:$_port/api/health") || return 1
+    case "$_resp" in
+        *'"status"'*'"healthy"'*'"service"'*'"Unsloth UI Backend"'*) return 0 ;;
+        *'"service"'*'"Unsloth UI Backend"'*'"status"'*'"healthy"'*) return 0 ;;
+    esac
+    return 1
+}
+
+# ── Port scanning ──
+_candidate_ports() {
+    echo "$BASE_PORT"
+    _max_port=$((BASE_PORT + MAX_PORT_OFFSET))
+    if command -v ss >/dev/null 2>&1; then
+        ss -tlnH 2>/dev/null | awk '{print $4}' | grep -oE '[0-9]+$' | \
+            awk -v lo="$BASE_PORT" -v hi="$_max_port" '$1 >= lo && $1 <= hi && $1 != lo {print}' || true
+    elif command -v lsof >/dev/null 2>&1; then
+        lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null | awk '{print $9}' | grep -oE '[0-9]+$' | \
+            awk -v lo="$BASE_PORT" -v hi="$_max_port" '$1 >= lo && $1 <= hi && $1 != lo {print}' || true
+    else
+        _offset=1
+        while [ "$_offset" -le "$MAX_PORT_OFFSET" ]; do
+            echo $((BASE_PORT + _offset))
+            _offset=$((_offset + 1))
+        done
+    fi
+}
+
+_find_healthy_port() {
+    for _p in $(_candidate_ports | sort -un); do
+        if _check_health "$_p"; then
+            echo "$_p"
+            return 0
+        fi
+    done
+    return 1
+}
+
+# ── Check if a port is busy ──
+_is_port_busy() {
+    _port=$1
+    if command -v ss >/dev/null 2>&1; then
+        ss -tlnH 2>/dev/null | awk '{print $4}' | grep -qE "[.:]$_port$"
+    elif command -v lsof >/dev/null 2>&1; then
+        lsof -iTCP:"$_port" -sTCP:LISTEN -nP >/dev/null 2>&1
+    else
+        return 1
+    fi
+}
+
+# ── Find a free port in range ──
+_find_launch_port() {
+    _offset=0
+    while [ "$_offset" -le "$MAX_PORT_OFFSET" ]; do
+        _candidate=$((BASE_PORT + _offset))
+        if ! _is_port_busy "$_candidate"; then
+            echo "$_candidate"
+            return 0
+        fi
+        _offset=$((_offset + 1))
+    done
+    return 1
+}
+
+# ── Open browser ──
+_open_browser() {
+    _url="$1"
+    if [ "$(uname)" = "Darwin" ] && command -v open >/dev/null 2>&1; then
+        open "$_url"
+    elif command -v xdg-open >/dev/null 2>&1; then
+        xdg-open "$_url" >/dev/null 2>&1 &
+    else
+        echo "Open in your browser: $_url" >&2
+    fi
+}
+
+# ── Spawn terminal with studio command ──
+_spawn_terminal() {
+    _cmd="$1"
+    _os=$(uname)
+    if [ "$_os" = "Darwin" ]; then
+        # Escape backslashes and double-quotes for AppleScript string
+        _cmd_escaped=$(printf '%s' "$_cmd" | sed 's/\\/\\\\/g; s/"/\\"/g')
+        osascript -e "tell application \"Terminal\" to do script \"$_cmd_escaped\"" >/dev/null 2>&1 && return 0
+    else
+        for _term in gnome-terminal konsole xfce4-terminal mate-terminal lxterminal xterm; do
+            if command -v "$_term" >/dev/null 2>&1; then
+                case "$_term" in
+                    gnome-terminal) "$_term" -- sh -c "$_cmd" & return 0 ;;
+                    konsole)        "$_term" -e sh -c "$_cmd" & return 0 ;;
+                    xterm)          "$_term" -e sh -c "$_cmd" & return 0 ;;
+                    *)              "$_term" -e sh -c "$_cmd" & return 0 ;;
+                esac
+            fi
+        done
+    fi
+    # Fallback: background with log
+    echo "No terminal emulator found; running in background. Logs: $LOG_FILE" >&2
+    nohup sh -c "$_cmd" >> "$LOG_FILE" 2>&1 &
+    return 0
+}
+
+# ── Atomic directory-based single-instance guard ──
+_acquire_lock() {
+    if mkdir "$LOCK_DIR" 2>/dev/null; then
+        echo "$$" > "$LOCK_DIR/pid"
+        return 0
+    fi
+
+    # Lock dir exists -- check if owner is still alive
+    _old_pid=$(cat "$LOCK_DIR/pid" 2>/dev/null || true)
+    if [ -n "$_old_pid" ] && kill -0 "$_old_pid" 2>/dev/null; then
+        # Another launcher is running; wait for it to bring Studio up
+        _deadline=$(($(date +%s) + TIMEOUT_SEC))
+        while [ "$(date +%s)" -lt "$_deadline" ]; do
+            _port=$(_find_healthy_port) && {
+                _open_browser "http://localhost:$_port"
+                exit 0
+            }
+            sleep "$POLL_INTERVAL_SEC"
+        done
+        echo "Timed out waiting for other launcher (PID $_old_pid)" >&2
+        exit 0
+    fi
+
+    # Stale lock -- reclaim
+    rm -rf "$LOCK_DIR"
+    mkdir "$LOCK_DIR" 2>/dev/null || return 1
+    echo "$$" > "$LOCK_DIR/pid"
+}
+
+_release_lock() {
+    rm -rf "$LOCK_DIR"
+}
+
+# ── Main ──
+# Fast path: already healthy
+_port=$(_find_healthy_port) && {
+    _open_browser "http://localhost:$_port"
+    exit 0
+}
+
+_acquire_lock
+trap '_release_lock' EXIT INT TERM
+
+# Post-lock re-check (handles race with another launcher)
+_port=$(_find_healthy_port) && {
+    _open_browser "http://localhost:$_port"
+    exit 0
+}
+
+# Find a free port in range
+_launch_port=$(_find_launch_port) || {
+    echo "No free port found in range ${BASE_PORT}-$((BASE_PORT + MAX_PORT_OFFSET))" >&2
+    exit 1
+}
+
+# Launch studio in a terminal
+_launch_cmd=$(printf '%q ' "$UNSLOTH_EXE" studio -H 0.0.0.0 -p "$_launch_port")
+_launch_cmd=${_launch_cmd% }
+_spawn_terminal "$_launch_cmd"
+
+# Poll for health
+_deadline=$(($(date +%s) + TIMEOUT_SEC))
+while [ "$(date +%s)" -lt "$_deadline" ]; do
+    _port=$(_find_healthy_port) && {
+        _open_browser "http://localhost:$_port"
+        exit 0
+    }
+    sleep "$POLL_INTERVAL_SEC"
+done
+
+echo "Unsloth Studio did not become healthy within ${TIMEOUT_SEC}s." >&2
+echo "Check logs at: $LOG_FILE" >&2
+exit 1
+LAUNCHER_EOF
+
+    chmod +x "$_css_launcher"
+
+    # Write the exe path to a separate conf file sourced by the launcher.
+    # Using single-quote wrapping with the standard '\'' escape for any
+    # embedded apostrophes. This avoids all sed metacharacter issues.
+    _css_quoted_exe=$(printf '%s' "$_css_exe" | sed "s/'/'\\\\''/g")
+    printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'" > "$_css_data_dir/studio.conf"
+
+    # ── Icon: try bundled, then download ──
+    # favicon.png (small, for Linux) and unsloth-gem.png (large, for macOS icns)
+    _css_script_dir=""
+    if [ -n "${0:-}" ] && [ -f "$0" ]; then
+        _css_script_dir=$(cd "$(dirname "$0")" 2>/dev/null && pwd) || true
+    fi
+
+    # Try to find favicon.png from installed package (site-packages) or local repo
+    _css_found_favicon=""
+    _css_found_gem=""
+    _css_venv_dir=$(dirname "$(dirname "$_css_exe")")
+    # Check site-packages
+    for _sp in "$_css_venv_dir"/lib/python*/site-packages/unsloth/studio/frontend/public; do
+        if [ -f "$_sp/favicon.png" ]; then
+            _css_found_favicon="$_sp/favicon.png"
+        fi
+        if [ -f "$_sp/unsloth-gem.png" ]; then
+            _css_found_gem="$_sp/unsloth-gem.png"
+        fi
+    done
+    # Check local repo (when running from clone)
+    if [ -z "$_css_found_favicon" ] && [ -n "$_css_script_dir" ] && [ -f "$_css_script_dir/studio/frontend/public/favicon.png" ]; then
+        _css_found_favicon="$_css_script_dir/studio/frontend/public/favicon.png"
+    fi
+    if [ -z "$_css_found_gem" ] && [ -n "$_css_script_dir" ] && [ -f "$_css_script_dir/studio/frontend/public/unsloth-gem.png" ]; then
+        _css_found_gem="$_css_script_dir/studio/frontend/public/unsloth-gem.png"
+    fi
+
+    # Copy or download favicon.png
+    if [ -n "$_css_found_favicon" ]; then
+        cp "$_css_found_favicon" "$_css_icon_png" 2>/dev/null || true
+    elif [ ! -f "$_css_icon_png" ]; then
+        download "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/favicon.png" "$_css_icon_png" 2>/dev/null || true
+    fi
+    # Copy or download unsloth-gem.png (for macOS icns)
+    if [ -n "$_css_found_gem" ]; then
+        cp "$_css_found_gem" "$_css_gem_png" 2>/dev/null || true
+    elif [ ! -f "$_css_gem_png" ]; then
+        download "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/frontend/public/unsloth-gem.png" "$_css_gem_png" 2>/dev/null || true
+    fi
+
+    # Validate PNG header (first 4 bytes: \x89PNG)
+    _css_validate_png() {
+        [ -f "$1" ] || return 1
+        _hdr=$(od -An -tx1 -N4 "$1" 2>/dev/null | tr -d ' ')
+        [ "$_hdr" = "89504e47" ]
+    }
+    if [ -f "$_css_icon_png" ] && ! _css_validate_png "$_css_icon_png"; then
+        rm -f "$_css_icon_png"
+    fi
+    if [ -f "$_css_gem_png" ] && ! _css_validate_png "$_css_gem_png"; then
+        rm -f "$_css_gem_png"
+    fi
+
+    # ── Platform-specific shortcuts ──
+    _css_created=0
+
+    if [ "$_css_os" = "linux" ]; then
+        # ── Linux: .desktop file ──
+        _css_app_dir="$HOME/.local/share/applications"
+        mkdir -p "$_css_app_dir"
+
+        _css_desktop="$_css_app_dir/unsloth-studio.desktop"
+        # Escape backslashes and double-quotes for .desktop Exec= field
+        _css_exec_escaped=$(printf '%s' "$_css_launcher" | sed 's/\\/\\\\/g; s/"/\\"/g')
+        _css_icon_escaped=$(printf '%s' "$_css_icon_png" | sed 's/\\/\\\\/g; s/"/\\"/g')
+        cat > "$_css_desktop" << DESKTOP_EOF
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Unsloth Studio
+Comment=Launch Unsloth Studio
+Exec="$_css_exec_escaped"
+Icon=$_css_icon_escaped
+Terminal=false
+StartupNotify=true
+Categories=Development;Science;
+DESKTOP_EOF
+        chmod +x "$_css_desktop"
+
+        # Copy to ~/Desktop if it exists
+        if [ -d "$HOME/Desktop" ]; then
+            cp "$_css_desktop" "$HOME/Desktop/unsloth-studio.desktop" 2>/dev/null || true
+            chmod +x "$HOME/Desktop/unsloth-studio.desktop" 2>/dev/null || true
+            # Mark as trusted so GNOME/Nautilus allows launching via double-click
+            if command -v gio >/dev/null 2>&1; then
+                gio set "$HOME/Desktop/unsloth-studio.desktop" metadata::trusted true 2>/dev/null || true
+            fi
+        fi
+
+        # Best-effort update database
+        update-desktop-database "$_css_app_dir" 2>/dev/null || true
+        _css_created=1
+
+    elif [ "$_css_os" = "macos" ]; then
+        # ── macOS: .app bundle ──
+        _css_app="$HOME/Applications/Unsloth Studio.app"
+        _css_contents="$_css_app/Contents"
+        _css_macos_dir="$_css_contents/MacOS"
+        _css_res_dir="$_css_contents/Resources"
+        mkdir -p "$_css_macos_dir" "$_css_res_dir"
+
+        # Info.plist
+        cat > "$_css_contents/Info.plist" << 'PLIST_EOF'
+
+
+
+
+    CFBundleIdentifier
+    ai.unsloth.studio
+    CFBundleName
+    Unsloth Studio
+    CFBundleDisplayName
+    Unsloth Studio
+    CFBundleExecutable
+    launch-studio
+    CFBundleIconFile
+    AppIcon
+    CFBundlePackageType
+    APPL
+    CFBundleVersion
+    1.0
+    CFBundleShortVersionString
+    1.0
+    LSMinimumSystemVersion
+    10.15
+    NSHighResolutionCapable
+    
+
+
+PLIST_EOF
+
+        # Executable stub
+        cat > "$_css_macos_dir/launch-studio" << STUB_EOF
+#!/bin/sh
+exec "$HOME/.local/share/unsloth/launch-studio.sh" "\$@"
+STUB_EOF
+        chmod +x "$_css_macos_dir/launch-studio"
+
+        # Build AppIcon.icns from unsloth-gem.png (2240x2240)
+        if [ -f "$_css_gem_png" ] && command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
+            _css_tmpdir=$(mktemp -d 2>/dev/null)
+            if [ -d "$_css_tmpdir" ]; then
+                _css_iconset="$_css_tmpdir/AppIcon.iconset"
+                mkdir -p "$_css_iconset"
+                _css_icon_ok=true
+                for _sz in 16 32 128 256 512; do
+                    _sz2=$((_sz * 2))
+                    sips -z "$_sz" "$_sz" "$_css_gem_png" --out "$_css_iconset/icon_${_sz}x${_sz}.png" >/dev/null 2>&1 || _css_icon_ok=false
+                    sips -z "$_sz2" "$_sz2" "$_css_gem_png" --out "$_css_iconset/icon_${_sz}x${_sz}@2x.png" >/dev/null 2>&1 || _css_icon_ok=false
+                done
+                if [ "$_css_icon_ok" = "true" ]; then
+                    iconutil -c icns "$_css_iconset" -o "$_css_res_dir/AppIcon.icns" 2>/dev/null || true
+                fi
+                rm -rf "$_css_tmpdir"
+            fi
+        fi
+        # Fallback: copy PNG as icon
+        if [ ! -f "$_css_res_dir/AppIcon.icns" ] && [ -f "$_css_icon_png" ]; then
+            cp "$_css_icon_png" "$_css_res_dir/AppIcon.icns" 2>/dev/null || true
+        fi
+
+        # Touch so Finder indexes it
+        touch "$_css_app"
+
+        # Symlink on Desktop
+        if [ -d "$HOME/Desktop" ]; then
+            ln -sf "$_css_app" "$HOME/Desktop/Unsloth Studio" 2>/dev/null || true
+        fi
+        _css_created=1
+    fi
+
+    if [ "$_css_created" -eq 1 ]; then
+        echo "[OK] Created Unsloth Studio shortcut(s)"
+    fi
+}
+
 echo ""
 echo "========================================="
 echo "   Unsloth Studio Installer"
@@ -251,6 +686,8 @@ echo "==> Running unsloth studio setup..."
 REQUESTED_PYTHON_VERSION="$(cd "$VENV_NAME/bin" && pwd)/python" \
 "$VENV_NAME/bin/unsloth" studio setup 
Date: Wed, 25 Mar 2026 13:27:41 +0200
Subject: [PATCH 19/34] perf(studio): upgrade to Vite 8 + auto-install bun for
 faster frontend builds (#4522)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* perf(studio): upgrade to Vite 8 + auto-install bun for 3x faster frontend builds

* fix(studio): make bun-to-npm fallback actually reachable

setup.sh used run_quiet() for the bun install attempt, but run_quiet
calls exit on failure. This killed the script before the npm fallback
could run, making the "falling back to npm" branch dead code.

Replace the run_quiet call with a direct bun invocation that captures
output to a temp file (same pattern, but returns instead of exiting).

Also clean up partial node_modules left by a failed bun install before
falling back to npm, in both setup.sh and build.sh. Without this, npm
inherits a corrupted node_modules tree from the failed bun run.

* fix(studio): restore commonjsOptions for dagre CJS interop

The previous commit removed build.commonjsOptions, assuming Vite 8's
Rolldown handles CJS natively. While optimizeDeps.include covers the
dev server (pre-bundling), it does NOT apply to production builds.

The resolve.alias still points @dagrejs/dagre to its .cjs.js entry,
so without commonjsOptions the production bundle fails to resolve
the CJS default export. This causes "TypeError: e is not a function"
on /chat after build (while dev mode works fine).

Restore the original commonjsOptions block to fix production builds.

* fix(studio): use motion/react instead of legacy framer-motion import

* fix(studio): address PR review findings for Vite 8 + bun upgrade

Fixes:
  - Remove bun.lock from repo and add to .gitignore (npm is source of truth)
  - Use & bun install *> $null pattern in setup.ps1 for reliable $LASTEXITCODE
  - Add Remove-Item node_modules before npm fallback in setup.ps1
  - Print bun install failure log in setup.sh before discarding
  - Add Refresh-Environment after npm install -g bun in setup.ps1
  - Tighten Node version check to ^20.19.0 || >=22.12.0 (Vite 8 requirement)
  - Add engines field to package.json
  - Use string comparison for _install_ok in build.sh
  - Remove explicit framer-motion ^11.18.2 from package.json (motion pulls
    framer-motion ^12.38.0 as its own dependency — the old pin caused a
    version conflict)

* Fix Colab Node bypass and bun.lock stale-build trigger

Gate the Colab Node shortcut on NODE_OK=true so Colab
environments with a Node version too old for Vite 8 fall
through to the nvm install path instead of silently proceeding.

Exclude bun.lock from the stale-build probe in both setup.sh
and setup.ps1 so it does not force unnecessary frontend rebuilds
on every run.

---------

Co-authored-by: Daniel Han 
Co-authored-by: Shine1i 
---
 build.sh                                      |   17 +-
 studio/frontend/.gitignore                    |    1 +
 studio/frontend/bun.lock                      | 2483 -----------------
 studio/frontend/package.json                  |   10 +-
 .../src/components/assistant-ui/thread.tsx    |    2 +-
 studio/setup.ps1                              |   81 +-
 studio/setup.sh                               |   47 +-
 7 files changed, 131 insertions(+), 2510 deletions(-)
 delete mode 100644 studio/frontend/bun.lock

diff --git a/build.sh b/build.sh
index 3118e8810a..cf8aa02910 100644
--- a/build.sh
+++ b/build.sh
@@ -29,7 +29,22 @@ _restore_gitignores() {
 }
 trap _restore_gitignores EXIT
 
-npm install
+# Use bun for install if available (faster), fall back to npm.
+_install_ok=false
+if command -v bun &>/dev/null; then
+    if bun install; then
+        _install_ok=true
+    else
+        echo "⚠ bun install failed, falling back to npm"
+        rm -rf node_modules
+    fi
+fi
+if [ "$_install_ok" != "true" ]; then
+    if ! npm install; then
+        echo "❌ ERROR: package install failed" >&2
+        exit 1
+    fi
+fi
 npm run build       # outputs to studio/frontend/dist/
 
 _restore_gitignores
diff --git a/studio/frontend/.gitignore b/studio/frontend/.gitignore
index bf7ac45ef1..f43950477e 100644
--- a/studio/frontend/.gitignore
+++ b/studio/frontend/.gitignore
@@ -11,6 +11,7 @@ pnpm-debug.log*
 lerna-debug.log*
 
 node_modules
+bun.lock
 dist
 dist-ssr
 test/
diff --git a/studio/frontend/bun.lock b/studio/frontend/bun.lock
deleted file mode 100644
index 5504aea3d3..0000000000
--- a/studio/frontend/bun.lock
+++ /dev/null
@@ -1,2483 +0,0 @@
-{
-  "lockfileVersion": 1,
-  "configVersion": 1,
-  "workspaces": {
-    "": {
-      "name": "unsloth-theme",
-      "dependencies": {
-        "@assistant-ui/react": "^0.12.19",
-        "@assistant-ui/react-markdown": "^0.12.3",
-        "@assistant-ui/react-streamdown": "^0.1.2",
-        "@base-ui/react": "^1.2.0",
-        "@dagrejs/dagre": "^2.0.4",
-        "@dagrejs/graphlib": "^3.0.4",
-        "@fontsource-variable/figtree": "^5.2.10",
-        "@fontsource-variable/inter": "^5.2.8",
-        "@fontsource-variable/space-grotesk": "^5.2.10",
-        "@hugeicons/core-free-icons": "^3.1.1",
-        "@hugeicons/react": "^1.1.5",
-        "@huggingface/hub": "^2.9.0",
-        "@langchain/core": "^1.1.27",
-        "@radix-ui/react-checkbox": "^1.3.3",
-        "@radix-ui/react-label": "^2.1.8",
-        "@radix-ui/react-select": "^2.2.6",
-        "@radix-ui/react-separator": "^1.1.8",
-        "@radix-ui/react-slot": "^1.2.4",
-        "@streamdown/cjk": "1.0.2",
-        "@streamdown/code": "1.0.2",
-        "@streamdown/math": "1.0.2",
-        "@streamdown/mermaid": "1.0.2",
-        "@tailwindcss/vite": "^4.1.18",
-        "@tanstack/react-router": "^1.159.10",
-        "@tanstack/react-table": "^8.21.3",
-        "@toolwind/corner-shape": "^0.0.8-3",
-        "@types/canvas-confetti": "^1.9.0",
-        "@xyflow/react": "^12.10.0",
-        "assistant-stream": "^0.3.2",
-        "canvas-confetti": "^1.9.4",
-        "class-variance-authority": "^0.7.1",
-        "clsx": "^2.1.1",
-        "cmdk": "^1.1.1",
-        "date-fns": "^4.1.0",
-        "dexie": "^4.3.0",
-        "framer-motion": "^11.18.2",
-        "js-yaml": "^4.1.1",
-        "katex": "^0.16.28",
-        "lucide-react": "^0.577.0",
-        "mammoth": "^1.11.0",
-        "motion": "^12.34.0",
-        "next": "^16.1.6",
-        "next-themes": "^0.4.6",
-        "radix-ui": "^1.4.3",
-        "react": "^19.2.4",
-        "react-day-picker": "^9.13.2",
-        "react-dom": "^19.2.4",
-        "react-resizable-panels": "^4.6.4",
-        "recharts": "3.7.0",
-        "remark-gfm": "^4.0.1",
-        "shadcn": "^3.8.4",
-        "sonner": "^2.0.7",
-        "streamdown": "2.3.0",
-        "tailwind-merge": "^3.4.0",
-        "tailwindcss": "^4.1.18",
-        "tw-animate-css": "^1.4.0",
-        "tw-shimmer": "^0.4.6",
-        "unpdf": "^1.4.0",
-        "zustand": "^5.0.11",
-      },
-      "devDependencies": {
-        "@biomejs/biome": "^1.9.4",
-        "@eslint/js": "^9.39.1",
-        "@types/js-yaml": "^4.0.9",
-        "@types/node": "^24.10.1",
-        "@types/react": "^19.2.5",
-        "@types/react-dom": "^19.2.3",
-        "@vitejs/plugin-react": "^5.1.1",
-        "eslint": "^9.39.1",
-        "eslint-plugin-react-hooks": "^7.0.1",
-        "eslint-plugin-react-refresh": "^0.4.26",
-        "globals": "^16.5.0",
-        "typescript": "~5.9.3",
-        "typescript-eslint": "^8.55.0",
-        "vite": "^7.3.1",
-      },
-    },
-  },
-  "packages": {
-    "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
-
-    "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="],
-
-    "@assistant-ui/core": ["@assistant-ui/core@0.1.7", "", { "dependencies": { "assistant-stream": "^0.3.6", "nanoid": "^5.1.6" }, "peerDependencies": { "@assistant-ui/store": "^0.2.3", "@assistant-ui/tap": "^0.5.3", "@types/react": "*", "assistant-cloud": "^0.1.22", "react": "^18 || ^19", "zustand": "^5.0.11" }, "optionalPeers": ["@types/react", "assistant-cloud", "react", "zustand"] }, "sha512-219T42ihVOicbJXZLWgD2CW5Bylg9Nk7geC331X4RfJxTDYlm2zIjViGlGaqfj6URXBp6kMulO2BTUrHGmAvdw=="],
-
-    "@assistant-ui/react": ["@assistant-ui/react@0.12.19", "", { "dependencies": { "@assistant-ui/core": "^0.1.7", "@assistant-ui/store": "^0.2.3", "@assistant-ui/tap": "^0.5.3", "@radix-ui/primitive": "^1.1.3", "@radix-ui/react-compose-refs": "^1.1.2", "@radix-ui/react-context": "^1.1.3", "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "@radix-ui/react-use-escape-keydown": "^1.1.1", "assistant-cloud": "^0.1.22", "assistant-stream": "^0.3.6", "nanoid": "^5.1.6", "radix-ui": "^1.4.3", "react-textarea-autosize": "^8.5.9", "zod": "^4.3.6", "zustand": "^5.0.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-scAf0o8cwjuHT9Y44EFGXcE2y6BSmpeMvt0NxOn8+Y/HBlNttQMLNvrM0p2AjacXCUufagiafAnWybzBV3nKEQ=="],
-
-    "@assistant-ui/react-markdown": ["@assistant-ui/react-markdown@0.12.4", "", { "dependencies": { "@radix-ui/react-primitive": "^2.1.4", "@radix-ui/react-use-callback-ref": "^1.1.1", "classnames": "^2.5.1", "react-markdown": "^10.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.11", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-6TD9guiuLJxJoOwSjNHUYAVma2ctDCG9uypUqKHE0OUhDwTDD3NsMvTnQ0n0Lh8nnCEwVglOwKKlSEYpV7SnWA=="],
-
-    "@assistant-ui/react-streamdown": ["@assistant-ui/react-streamdown@0.1.3", "", { "dependencies": { "rehype-harden": "^1.1.7", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "streamdown": "^2.1.0" }, "peerDependencies": { "@assistant-ui/react": "^0.12.11", "@streamdown/cjk": "^1.0.0", "@streamdown/code": "^1.0.0", "@streamdown/math": "^1.0.0", "@streamdown/mermaid": "^1.0.0", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@streamdown/cjk", "@streamdown/code", "@streamdown/math", "@streamdown/mermaid", "@types/react"] }, "sha512-n1UCjXQ3svmDtJBMJj/vXqz/BqAQBuy7myrXeymz2tD9l+ENQgqu2JY5ir3J19juJTe5lsi/P3+tOJ2C1jc/nw=="],
-
-    "@assistant-ui/store": ["@assistant-ui/store@0.2.3", "", { "dependencies": { "use-effect-event": "^2.0.3" }, "peerDependencies": { "@assistant-ui/tap": "^0.5.3", "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-daStbgSQiX7+csqK6Cvo7A8p8UZkTCSMxBHxbhJvwrlVbp7BRJWTxq3U3rpTkSGIar23SXIyVRRfXU8VW7pswA=="],
-
-    "@assistant-ui/tap": ["@assistant-ui/tap@0.5.3", "", { "peerDependencies": { "@types/react": "*", "react": "^18 || ^19" }, "optionalPeers": ["@types/react", "react"] }, "sha512-wy06ksqF2LfFxe4JXy31Ns89N/be1Dy3c+mG363cFHFp3CbLkRu8CrCN2SQSgCkXt628E+D8QyzqdBcl9kD4NQ=="],
-
-    "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
-
-    "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
-
-    "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
-
-    "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
-
-    "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
-
-    "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
-
-    "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="],
-
-    "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
-
-    "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
-
-    "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
-
-    "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
-
-    "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
-
-    "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
-
-    "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="],
-
-    "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
-
-    "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
-
-    "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
-
-    "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
-
-    "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
-
-    "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
-
-    "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
-
-    "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
-
-    "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
-
-    "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
-
-    "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
-
-    "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
-
-    "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
-
-    "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
-
-    "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
-
-    "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
-
-    "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
-
-    "@base-ui/react": ["@base-ui/react@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw=="],
-
-    "@base-ui/utils": ["@base-ui/utils@0.2.5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="],
-
-    "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
-
-    "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
-
-    "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
-
-    "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
-
-    "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
-
-    "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
-
-    "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
-
-    "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
-
-    "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
-
-    "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
-
-    "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="],
-
-    "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.1.1", "", { "dependencies": { "@chevrotain/gast": "11.1.1", "@chevrotain/types": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw=="],
-
-    "@chevrotain/gast": ["@chevrotain/gast@11.1.1", "", { "dependencies": { "@chevrotain/types": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg=="],
-
-    "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.1.1", "", {}, "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg=="],
-
-    "@chevrotain/types": ["@chevrotain/types@11.1.1", "", {}, "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw=="],
-
-    "@chevrotain/utils": ["@chevrotain/utils@11.1.1", "", {}, "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ=="],
-
-    "@dagrejs/dagre": ["@dagrejs/dagre@2.0.4", "", { "dependencies": { "@dagrejs/graphlib": "3.0.4" } }, "sha512-J6vCWTNpicHF4zFlZG1cS5DkGzMr9941gddYkakjrg3ZNev4bbqEgLHFTWiFrcJm7UCRu7olO3K6IRDd9gSGhA=="],
-
-    "@dagrejs/graphlib": ["@dagrejs/graphlib@3.0.4", "", {}, "sha512-HxZ7fCvAwTLCWCO0WjDkzAFQze8LdC6iOpKbetDKHIuDfIgMlIzYzqZ4nxwLlclQX+3ZVeZ1K2OuaOE2WWcyOg=="],
-
-    "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="],
-
-    "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.52.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w=="],
-
-    "@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="],
-
-    "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
-
-    "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
-
-    "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
-
-    "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
-
-    "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
-
-    "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
-
-    "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
-
-    "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
-
-    "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
-
-    "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
-
-    "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
-
-    "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
-
-    "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
-
-    "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
-
-    "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
-
-    "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
-
-    "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
-
-    "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
-
-    "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
-
-    "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
-
-    "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
-
-    "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
-
-    "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
-
-    "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
-
-    "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
-
-    "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
-
-    "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
-
-    "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
-
-    "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
-
-    "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
-
-    "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
-
-    "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
-
-    "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="],
-
-    "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="],
-
-    "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
-
-    "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
-
-    "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
-
-    "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
-
-    "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
-
-    "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
-
-    "@fontsource-variable/figtree": ["@fontsource-variable/figtree@5.2.10", "", {}, "sha512-a5Gumbpy3mdd+Yg31g6Qb7CmjYbrfyutJa3bWfP5q8A4GclIOwX7mI+ZuSHsJnw/mHvW6r9oh1AHJcJTIxK4JA=="],
-
-    "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="],
-
-    "@fontsource-variable/space-grotesk": ["@fontsource-variable/space-grotesk@5.2.10", "", {}, "sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w=="],
-
-    "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
-
-    "@hugeicons/core-free-icons": ["@hugeicons/core-free-icons@3.1.1", "", {}, "sha512-UpS2lUQFi5sKyJSWwM6rO+BnPLvVz1gsyCpPHeZyVuZqi89YH8ksliza4cwaODqKOZyeXmG8juo1ty4QtQofkg=="],
-
-    "@hugeicons/react": ["@hugeicons/react@1.1.5", "", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-JX/iDz3oO7hWdVqbjwFwRrAjHk8h2vI+mBkNzp4JcXG3t4idoupfjon73nLOA7cr27m0M8hrRC1Q2h6nEBGKVA=="],
-
-    "@huggingface/hub": ["@huggingface/hub@2.10.3", "", { "dependencies": { "@huggingface/tasks": "^0.19.85" }, "optionalDependencies": { "cli-progress": "^3.12.0" }, "bin": { "hfjs": "dist/cli.js" } }, "sha512-qSk4FcVFdTGx0lNpFyy7p2KwgAPCsjM2+tupG/MGToEvUGVLsy+dCmela1BcU/VvJNweCtnH5HwdNr7IQa4Zzw=="],
-
-    "@huggingface/tasks": ["@huggingface/tasks@0.19.86", "", {}, "sha512-eab/6J9m+0Z8xw3X2EPPioMLIjFNYjox9nONTmzzgWj0vq6+iMWsMt4tlwrZKLlxxJbFp+acn20VXZi3ejLlng=="],
-
-    "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
-
-    "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
-
-    "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
-
-    "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
-
-    "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
-
-    "@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
-
-    "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
-
-    "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
-
-    "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
-
-    "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
-
-    "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
-
-    "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
-
-    "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
-
-    "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
-
-    "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
-
-    "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
-
-    "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
-
-    "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
-
-    "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
-
-    "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
-
-    "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
-
-    "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
-
-    "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
-
-    "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
-
-    "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
-
-    "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
-
-    "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
-
-    "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
-
-    "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
-
-    "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
-
-    "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
-
-    "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="],
-
-    "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="],
-
-    "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="],
-
-    "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="],
-
-    "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
-
-    "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
-
-    "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
-
-    "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
-
-    "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
-
-    "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
-
-    "@langchain/core": ["@langchain/core@1.1.28", "", { "dependencies": { "@cfworker/json-schema": "^4.0.2", "ansi-styles": "^5.0.0", "camelcase": "6", "decamelize": "1.2.0", "js-tiktoken": "^1.0.12", "langsmith": ">=0.5.0 <1.0.0", "mustache": "^4.2.0", "p-queue": "^6.6.2", "uuid": "^10.0.0", "zod": "^3.25.76 || ^4" } }, "sha512-6FAGdezEp8zHY92LtnsAiv54KaG41nBdsuukk+R+1484edV20cVOyIc36ANuGKPx0pmYFCBWhCUdO0jxB/zn2Q=="],
-
-    "@mermaid-js/parser": ["@mermaid-js/parser@1.0.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw=="],
-
-    "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
-
-    "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="],
-
-    "@next/env": ["@next/env@16.1.6", "", {}, "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ=="],
-
-    "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.1.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw=="],
-
-    "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.1.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ=="],
-
-    "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw=="],
-
-    "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.1.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ=="],
-
-    "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ=="],
-
-    "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.1.6", "", { "os": "linux", "cpu": "x64" }, "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg=="],
-
-    "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.1.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw=="],
-
-    "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.1.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A=="],
-
-    "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
-
-    "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="],
-
-    "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
-
-    "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
-
-    "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
-
-    "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
-
-    "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="],
-
-    "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="],
-
-    "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="],
-
-    "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
-
-    "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
-
-    "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
-
-    "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
-
-    "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
-
-    "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
-
-    "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
-
-    "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
-
-    "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
-
-    "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
-
-    "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
-
-    "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
-
-    "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
-
-    "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
-
-    "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
-
-    "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
-
-    "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
-
-    "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
-
-    "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
-
-    "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
-
-    "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
-
-    "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
-
-    "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
-
-    "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
-
-    "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
-
-    "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
-
-    "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
-
-    "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
-
-    "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
-
-    "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
-
-    "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
-
-    "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
-
-    "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
-
-    "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
-
-    "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
-
-    "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
-
-    "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
-
-    "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
-
-    "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
-
-    "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
-
-    "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
-
-    "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
-
-    "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
-
-    "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
-
-    "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
-
-    "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
-
-    "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
-
-    "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
-
-    "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
-
-    "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
-
-    "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
-
-    "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
-
-    "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
-
-    "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
-
-    "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
-
-    "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
-
-    "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
-
-    "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
-
-    "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
-
-    "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
-
-    "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
-
-    "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
-
-    "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
-
-    "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
-
-    "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
-
-    "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
-
-    "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
-
-    "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
-
-    "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
-
-    "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
-
-    "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
-
-    "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
-
-    "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
-
-    "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
-
-    "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
-
-    "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
-
-    "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
-
-    "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
-
-    "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
-
-    "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
-
-    "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
-
-    "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
-
-    "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
-
-    "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
-
-    "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
-
-    "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
-
-    "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
-
-    "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
-
-    "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
-
-    "@shikijs/core": ["@shikijs/core@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-iAlTtSDDbJiRpvgL5ugKEATDtHdUVkqgHDm/gbD2ZS9c88mx7G1zSYjjOxp5Qa0eaW0MAQosFRmJSk354PRoQA=="],
-
-    "@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-jdKhfgW9CRtj3Tor0L7+yPwdG3CgP7W+ZEqSsojrMzCjD1e0IxIbwUMDDpYlVBlC08TACg4puwFGkZfLS+56Tw=="],
-
-    "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA=="],
-
-    "@shikijs/langs": ["@shikijs/langs@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA=="],
-
-    "@shikijs/themes": ["@shikijs/themes@3.22.0", "", { "dependencies": { "@shikijs/types": "3.22.0" } }, "sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g=="],
-
-    "@shikijs/types": ["@shikijs/types@3.22.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg=="],
-
-    "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
-
-    "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
-
-    "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
-
-    "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
-
-    "@streamdown/cjk": ["@streamdown/cjk@1.0.2", "", { "dependencies": { "remark-cjk-friendly": "^1.2.3", "remark-cjk-friendly-gfm-strikethrough": "^1.2.3", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-5OOuZjj2Lnae92Zmg2gA5hloSbcKj25gv+QY4iKbYI+iRsiGWbgmYxmgxNUSO9SR6BKOCy783UHN1HM/QEUpdw=="],
-
-    "@streamdown/code": ["@streamdown/code@1.0.2", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-QKLS3sC8no5y0YvhGLA+ZjtNhznWU09IvFcjRKgSA35ulckMLw3b5T1ha+o1DaW8BS8l0zceLPFZa3/X9+agWQ=="],
-
-    "@streamdown/math": ["@streamdown/math@1.0.2", "", { "dependencies": { "katex": "^0.16.27", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g=="],
-
-    "@streamdown/mermaid": ["@streamdown/mermaid@1.0.2", "", { "dependencies": { "mermaid": "^11.12.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw=="],
-
-    "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
-
-    "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
-
-    "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="],
-
-    "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg=="],
-
-    "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw=="],
-
-    "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw=="],
-
-    "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA=="],
-
-    "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw=="],
-
-    "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ=="],
-
-    "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ=="],
-
-    "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g=="],
-
-    "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.1", "", { "os": "linux", "cpu": "x64" }, "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g=="],
-
-    "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.1", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q=="],
-
-    "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA=="],
-
-    "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ=="],
-
-    "@tailwindcss/vite": ["@tailwindcss/vite@4.2.1", "", { "dependencies": { "@tailwindcss/node": "4.2.1", "@tailwindcss/oxide": "4.2.1", "tailwindcss": "4.2.1" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w=="],
-
-    "@tanstack/history": ["@tanstack/history@1.161.4", "", {}, "sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww=="],
-
-    "@tanstack/react-router": ["@tanstack/react-router@1.162.9", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.162.9", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-APbwKAF+YgSNpHAaA+FdgrmfI/7+qa9hApuVO9+P0IVksJayNIWFQ/6AFG90WQiTYWk64RI1R9cFV2K9Z+j2pQ=="],
-
-    "@tanstack/react-store": ["@tanstack/react-store@0.9.1", "", { "dependencies": { "@tanstack/store": "0.9.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA=="],
-
-    "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
-
-    "@tanstack/router-core": ["@tanstack/router-core@1.162.9", "", { "dependencies": { "@tanstack/history": "1.161.4", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-eG7C0oVtZbFOkfvsaF8UyGuNjEc1BfIfD5EzQNwG4vqLKOAyY5SMFBCNjabAi2sglRhL0ZOwKon1SExusU5fxA=="],
-
-    "@tanstack/store": ["@tanstack/store@0.9.1", "", {}, "sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg=="],
-
-    "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
-
-    "@toolwind/corner-shape": ["@toolwind/corner-shape@0.0.8-3", "", { "dependencies": { "@types/node": "^20.4.1" } }, "sha512-MPIF81F2bhtXbzEeXF0vnL+PKpnopCHOzBspOkK8osMzWQvPUujZn2XZOMdsu4DF6wsVbbRYQtdsJr486HmIPQ=="],
-
-    "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
-
-    "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
-
-    "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
-
-    "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
-
-    "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
-
-    "@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
-
-    "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
-
-    "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
-
-    "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
-
-    "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
-
-    "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
-
-    "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
-
-    "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
-
-    "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
-
-    "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
-
-    "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
-
-    "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
-
-    "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
-
-    "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
-
-    "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
-
-    "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
-
-    "@types/d3-geo": ["@types/d3-geo@3.1.0", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ=="],
-
-    "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
-
-    "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
-
-    "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
-
-    "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
-
-    "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
-
-    "@types/d3-random": ["@types/d3-random@3.0.3", "", {}, "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ=="],
-
-    "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
-
-    "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
-
-    "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="],
-
-    "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
-
-    "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
-
-    "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
-
-    "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
-
-    "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
-
-    "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="],
-
-    "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
-
-    "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
-
-    "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
-
-    "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
-
-    "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
-
-    "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
-
-    "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
-
-    "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="],
-
-    "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
-
-    "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
-
-    "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="],
-
-    "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
-
-    "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
-
-    "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
-
-    "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
-
-    "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
-
-    "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
-
-    "@types/uuid": ["@types/uuid@10.0.0", "", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
-
-    "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="],
-
-    "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
-
-    "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
-
-    "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
-
-    "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
-
-    "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
-
-    "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
-
-    "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
-
-    "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
-
-    "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
-
-    "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
-
-    "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
-
-    "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="],
-
-    "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
-
-    "@xyflow/react": ["@xyflow/react@12.10.1", "", { "dependencies": { "@xyflow/system": "0.0.75", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q=="],
-
-    "@xyflow/system": ["@xyflow/system@0.0.75", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ=="],
-
-    "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
-
-    "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
-
-    "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
-
-    "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
-
-    "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
-
-    "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
-
-    "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
-
-    "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
-
-    "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="],
-
-    "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
-
-    "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
-
-    "assistant-cloud": ["assistant-cloud@0.1.22", "", { "dependencies": { "assistant-stream": "^0.3.6" } }, "sha512-AEE9shV+oFrGDv/MRTRERctNKpIYS0n34UpAQXXICiOkSWD6QZnS1ljLqruFko7fJoT5CIWq8dNeJWdzQLTBLg=="],
-
-    "assistant-stream": ["assistant-stream@0.3.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-Ne/uTseMIiZx740dTbr/SWxONM8nYj4Z5BRmUfqQN+TNgtOCgWOlC/oTUQ+A7LIUHtmGbcoyZwDf8yd2RASnDA=="],
-
-    "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
-
-    "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
-
-    "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
-
-    "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
-
-    "baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
-
-    "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
-
-    "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
-
-    "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
-
-    "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
-
-    "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
-
-    "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
-
-    "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
-
-    "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
-
-    "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
-
-    "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
-
-    "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="],
-
-    "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
-
-    "canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="],
-
-    "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
-
-    "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
-
-    "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
-
-    "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
-
-    "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
-
-    "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
-
-    "chevrotain": ["chevrotain@11.1.1", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.1.1", "@chevrotain/gast": "11.1.1", "@chevrotain/regexp-to-ast": "11.1.1", "@chevrotain/types": "11.1.1", "@chevrotain/utils": "11.1.1", "lodash-es": "4.17.23" } }, "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ=="],
-
-    "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
-
-    "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
-
-    "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="],
-
-    "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
-
-    "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
-
-    "cli-progress": ["cli-progress@3.12.0", "", { "dependencies": { "string-width": "^4.2.3" } }, "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A=="],
-
-    "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
-
-    "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
-
-    "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
-
-    "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
-
-    "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
-
-    "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
-
-    "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="],
-
-    "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
-
-    "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
-
-    "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
-
-    "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
-
-    "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
-
-    "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
-
-    "console-table-printer": ["console-table-printer@2.15.0", "", { "dependencies": { "simple-wcswidth": "^1.1.2" } }, "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw=="],
-
-    "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
-
-    "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
-
-    "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
-
-    "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
-
-    "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="],
-
-    "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
-
-    "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
-
-    "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
-
-    "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
-
-    "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="],
-
-    "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
-
-    "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
-
-    "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
-
-    "cytoscape": ["cytoscape@3.33.1", "", {}, "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ=="],
-
-    "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
-
-    "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
-
-    "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
-
-    "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
-
-    "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
-
-    "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
-
-    "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
-
-    "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
-
-    "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
-
-    "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
-
-    "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
-
-    "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
-
-    "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
-
-    "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
-
-    "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
-
-    "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
-
-    "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
-
-    "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
-
-    "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
-
-    "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
-
-    "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
-
-    "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
-
-    "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
-
-    "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
-
-    "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
-
-    "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
-
-    "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
-
-    "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
-
-    "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
-
-    "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
-
-    "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
-
-    "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
-
-    "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
-
-    "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
-
-    "dagre-d3-es": ["dagre-d3-es@7.0.13", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q=="],
-
-    "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
-
-    "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
-
-    "date-fns-jalali": ["date-fns-jalali@4.1.0-0", "", {}, "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg=="],
-
-    "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
-
-    "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
-
-    "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
-
-    "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
-
-    "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
-
-    "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="],
-
-    "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
-
-    "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
-
-    "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
-
-    "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
-
-    "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
-
-    "delaunator": ["delaunator@5.0.1", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw=="],
-
-    "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
-
-    "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
-
-    "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
-
-    "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
-
-    "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
-
-    "dexie": ["dexie@4.3.0", "", {}, "sha512-5EeoQpJvMKHe6zWt/FSIIuRa3CWlZeIl6zKXt+Lz7BU6RoRRLgX9dZEynRfXrkLcldKYCBiz7xekTEylnie1Ug=="],
-
-    "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
-
-    "dingbat-to-unicode": ["dingbat-to-unicode@1.0.1", "", {}, "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w=="],
-
-    "dompurify": ["dompurify@3.3.1", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q=="],
-
-    "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
-
-    "duck": ["duck@0.1.12", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="],
-
-    "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
-
-    "eciesjs": ["eciesjs@0.4.17", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w=="],
-
-    "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
-
-    "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="],
-
-    "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
-
-    "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
-
-    "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
-
-    "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
-
-    "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
-
-    "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
-
-    "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
-
-    "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
-
-    "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
-
-    "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
-
-    "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
-
-    "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
-
-    "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
-
-    "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
-
-    "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="],
-
-    "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
-
-    "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
-
-    "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
-
-    "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
-
-    "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
-
-    "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
-
-    "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
-
-    "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
-
-    "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
-
-    "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
-
-    "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
-
-    "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
-
-    "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
-
-    "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
-
-    "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
-
-    "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
-
-    "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
-
-    "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
-
-    "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
-
-    "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
-
-    "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
-
-    "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
-
-    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
-
-    "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
-
-    "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
-
-    "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
-
-    "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
-
-    "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
-
-    "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
-
-    "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
-
-    "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
-
-    "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
-
-    "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
-
-    "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
-
-    "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
-
-    "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
-
-    "framer-motion": ["framer-motion@11.18.2", "", { "dependencies": { "motion-dom": "^11.18.1", "motion-utils": "^11.18.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w=="],
-
-    "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
-
-    "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
-
-    "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
-
-    "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
-
-    "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="],
-
-    "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="],
-
-    "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
-
-    "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
-
-    "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
-
-    "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
-
-    "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
-
-    "get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="],
-
-    "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
-
-    "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
-
-    "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
-
-    "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
-
-    "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
-
-    "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
-
-    "graphql": ["graphql@16.13.0", "", {}, "sha512-uSisMYERbaB9bkA9M4/4dnqyktaEkf1kMHNKq/7DHyxVeWqHQ2mBmVqm5u6/FVHwF3iCNalKcg82Zfl+tffWoA=="],
-
-    "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
-
-    "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
-
-    "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
-
-    "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
-
-    "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
-
-    "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="],
-
-    "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="],
-
-    "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
-
-    "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="],
-
-    "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
-
-    "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
-
-    "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
-
-    "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
-
-    "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
-
-    "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
-
-    "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="],
-
-    "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
-
-    "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
-
-    "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
-
-    "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
-
-    "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
-
-    "hono": ["hono@4.12.2", "", {}, "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg=="],
-
-    "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
-
-    "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
-
-    "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
-
-    "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
-
-    "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
-
-    "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
-
-    "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
-
-    "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
-
-    "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
-
-    "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
-
-    "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
-
-    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
-
-    "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
-
-    "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
-
-    "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
-
-    "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
-
-    "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
-
-    "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
-
-    "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
-
-    "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
-
-    "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
-
-    "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
-
-    "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
-
-    "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
-
-    "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
-
-    "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
-
-    "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
-
-    "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
-
-    "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
-
-    "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
-
-    "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="],
-
-    "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
-
-    "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
-
-    "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="],
-
-    "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
-
-    "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
-
-    "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
-
-    "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
-
-    "isbot": ["isbot@5.1.35", "", {}, "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg=="],
-
-    "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
-
-    "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
-
-    "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
-
-    "js-tiktoken": ["js-tiktoken@1.0.21", "", { "dependencies": { "base64-js": "^1.5.1" } }, "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g=="],
-
-    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
-
-    "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
-
-    "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
-
-    "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
-
-    "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
-
-    "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
-
-    "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
-
-    "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
-
-    "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
-
-    "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
-
-    "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
-
-    "katex": ["katex@0.16.33", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA=="],
-
-    "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
-
-    "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
-
-    "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
-
-    "langium": ["langium@4.2.1", "", { "dependencies": { "chevrotain": "~11.1.1", "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", "vscode-uri": "~3.1.0" } }, "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ=="],
-
-    "langsmith": ["langsmith@0.5.6", "", { "dependencies": { "@types/uuid": "^10.0.0", "chalk": "^5.6.2", "console-table-printer": "^2.12.1", "p-queue": "^6.6.2", "semver": "^7.6.3", "uuid": "^10.0.0" }, "peerDependencies": { "@opentelemetry/api": "*", "@opentelemetry/exporter-trace-otlp-proto": "*", "@opentelemetry/sdk-trace-base": "*", "openai": "*" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-proto", "@opentelemetry/sdk-trace-base", "openai"] }, "sha512-T/RA2l2MsTYX0z1aW8rQ2hBQZEOuXV2v/6tkfG6R5EotJTKMpw1dERCbvP8ezOP8otyWfnNlQA88ZnMRsQ7CHA=="],
-
-    "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
-
-    "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
-
-    "lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
-
-    "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
-
-    "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
-
-    "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
-
-    "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
-
-    "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
-
-    "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
-
-    "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
-
-    "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
-
-    "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
-
-    "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
-
-    "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
-
-    "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
-
-    "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
-
-    "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
-
-    "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
-
-    "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
-
-    "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
-
-    "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
-
-    "lop": ["lop@0.4.2", "", { "dependencies": { "duck": "^0.1.12", "option": "~0.2.1", "underscore": "^1.13.1" } }, "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw=="],
-
-    "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
-
-    "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="],
-
-    "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
-
-    "mammoth": ["mammoth@1.11.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "argparse": "~1.0.3", "base64-js": "^1.5.1", "bluebird": "~3.4.0", "dingbat-to-unicode": "^1.0.1", "jszip": "^3.7.1", "lop": "^0.4.2", "path-is-absolute": "^1.0.0", "underscore": "^1.13.1", "xmlbuilder": "^10.0.0" }, "bin": { "mammoth": "bin/mammoth" } }, "sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ=="],
-
-    "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
-
-    "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="],
-
-    "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
-
-    "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
-
-    "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
-
-    "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
-
-    "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
-
-    "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
-
-    "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
-
-    "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
-
-    "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
-
-    "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="],
-
-    "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
-
-    "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
-
-    "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
-
-    "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
-
-    "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
-
-    "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
-
-    "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
-
-    "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
-
-    "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
-
-    "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
-
-    "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
-
-    "mermaid": ["mermaid@11.12.3", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ=="],
-
-    "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
-
-    "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
-
-    "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q=="],
-
-    "micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.3.0", "micromark-extension-cjk-friendly-util": "2.1.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-gSPnxgHDDqXYOBvQRq6lerrq9mjDhdtKn+7XETuXjxWcL62yZEfUdA28Ml1I2vDIPfAOIKLa0h2XDSGkInGHFQ=="],
-
-    "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@2.1.1", "", { "dependencies": { "get-east-asian-width": "^1.3.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg=="],
-
-    "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
-
-    "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
-
-    "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
-
-    "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
-
-    "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
-
-    "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
-
-    "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
-
-    "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="],
-
-    "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
-
-    "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
-
-    "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
-
-    "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
-
-    "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
-
-    "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
-
-    "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
-
-    "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
-
-    "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
-
-    "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
-
-    "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
-
-    "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
-
-    "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
-
-    "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
-
-    "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
-
-    "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
-
-    "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
-
-    "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
-
-    "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
-
-    "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
-
-    "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
-
-    "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
-
-    "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
-
-    "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
-
-    "minimatch": ["minimatch@3.1.3", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA=="],
-
-    "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
-
-    "mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
-
-    "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="],
-
-    "motion-dom": ["motion-dom@11.18.1", "", { "dependencies": { "motion-utils": "^11.18.1" } }, "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw=="],
-
-    "motion-utils": ["motion-utils@11.18.1", "", {}, "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="],
-
-    "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
-
-    "msw": ["msw@2.12.10", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-G3VUymSE0/iegFnuipujpwyTM2GuZAKXNeerUSrG2+Eg391wW63xFs5ixWsK9MWzr1AGoSkYGmyAzNgbR3+urw=="],
-
-    "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
-
-    "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
-
-    "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
-
-    "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
-
-    "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
-
-    "next": ["next@16.1.6", "", { "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.1.6", "@next/swc-darwin-x64": "16.1.6", "@next/swc-linux-arm64-gnu": "16.1.6", "@next/swc-linux-arm64-musl": "16.1.6", "@next/swc-linux-x64-gnu": "16.1.6", "@next/swc-linux-x64-musl": "16.1.6", "@next/swc-win32-arm64-msvc": "16.1.6", "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw=="],
-
-    "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
-
-    "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
-
-    "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
-
-    "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
-
-    "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
-
-    "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
-
-    "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
-
-    "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="],
-
-    "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
-
-    "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
-
-    "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
-
-    "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
-
-    "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="],
-
-    "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
-
-    "option": ["option@0.2.4", "", {}, "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A=="],
-
-    "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
-
-    "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="],
-
-    "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="],
-
-    "p-finally": ["p-finally@1.0.0", "", {}, "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="],
-
-    "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
-
-    "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
-
-    "p-queue": ["p-queue@6.6.2", "", { "dependencies": { "eventemitter3": "^4.0.4", "p-timeout": "^3.2.0" } }, "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ=="],
-
-    "p-timeout": ["p-timeout@3.2.0", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg=="],
-
-    "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
-
-    "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
-
-    "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
-
-    "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
-
-    "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
-
-    "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
-
-    "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
-
-    "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
-
-    "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
-
-    "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
-
-    "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
-
-    "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
-
-    "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
-
-    "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
-
-    "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
-
-    "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
-
-    "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
-
-    "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
-
-    "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
-
-    "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
-
-    "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
-
-    "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
-
-    "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="],
-
-    "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
-
-    "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
-
-    "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
-
-    "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
-
-    "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
-
-    "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
-
-    "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
-
-    "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
-
-    "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
-
-    "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
-
-    "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
-
-    "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
-
-    "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
-
-    "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
-
-    "react-day-picker": ["react-day-picker@9.13.2", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg=="],
-
-    "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
-
-    "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
-
-    "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
-
-    "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
-
-    "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
-
-    "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
-
-    "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
-
-    "react-resizable-panels": ["react-resizable-panels@4.6.5", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-pmQP6qv9KmsesNMvWVNvVfVJAwYSOWWbAOAtrPR8Cre20+j1NWIlyft0btjtDQE+OepXmI6g3VPrCXQY0oD7+Q=="],
-
-    "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
-
-    "react-textarea-autosize": ["react-textarea-autosize@8.5.9", "", { "dependencies": { "@babel/runtime": "^7.20.13", "use-composed-ref": "^1.3.0", "use-latest": "^1.2.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A=="],
-
-    "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
-
-    "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
-
-    "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="],
-
-    "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
-
-    "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
-
-    "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
-
-    "regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
-
-    "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
-
-    "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="],
-
-    "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="],
-
-    "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
-
-    "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
-
-    "remark-cjk-friendly": ["remark-cjk-friendly@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g=="],
-
-    "remark-cjk-friendly-gfm-strikethrough": ["remark-cjk-friendly-gfm-strikethrough@1.2.3", "", { "dependencies": { "micromark-extension-cjk-friendly-gfm-strikethrough": "1.2.3" }, "peerDependencies": { "@types/mdast": "^4.0.0", "unified": "^11.0.0" }, "optionalPeers": ["@types/mdast"] }, "sha512-bXfMZtsaomK6ysNN/UGRIcasQAYkC10NtPmP0oOHOV8YOhA2TXmwRXCku4qOzjIFxAPfish5+XS0eIug2PzNZA=="],
-
-    "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
-
-    "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="],
-
-    "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
-
-    "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
-
-    "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
-
-    "remend": ["remend@1.2.1", "", {}, "sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w=="],
-
-    "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
-
-    "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
-
-    "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
-
-    "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
-
-    "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
-
-    "rettime": ["rettime@0.10.1", "", {}, "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw=="],
-
-    "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
-
-    "robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
-
-    "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
-
-    "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
-
-    "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
-
-    "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
-
-    "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
-
-    "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
-
-    "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
-
-    "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
-
-    "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
-
-    "secure-json-parse": ["secure-json-parse@4.1.0", "", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
-
-    "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
-
-    "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
-
-    "seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
-
-    "seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="],
-
-    "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
-
-    "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
-
-    "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
-
-    "shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="],
-
-    "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
-
-    "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
-
-    "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
-
-    "shiki": ["shiki@3.22.0", "", { "dependencies": { "@shikijs/core": "3.22.0", "@shikijs/engine-javascript": "3.22.0", "@shikijs/engine-oniguruma": "3.22.0", "@shikijs/langs": "3.22.0", "@shikijs/themes": "3.22.0", "@shikijs/types": "3.22.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-LBnhsoYEe0Eou4e1VgJACes+O6S6QC0w71fCSp5Oya79inkwkm15gQ1UF6VtQ8j/taMDh79hAB49WUk8ALQW3g=="],
-
-    "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
-
-    "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
-
-    "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
-
-    "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
-
-    "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
-
-    "simple-wcswidth": ["simple-wcswidth@1.1.2", "", {}, "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw=="],
-
-    "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
-
-    "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
-
-    "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
-
-    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
-
-    "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
-
-    "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
-
-    "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
-
-    "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="],
-
-    "streamdown": ["streamdown@2.3.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.1", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw=="],
-
-    "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
-
-    "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
-
-    "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
-
-    "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
-
-    "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="],
-
-    "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
-
-    "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
-
-    "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
-
-    "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
-
-    "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
-
-    "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
-
-    "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
-
-    "stylis": ["stylis@4.3.6", "", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="],
-
-    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
-
-    "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
-
-    "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
-
-    "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
-
-    "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
-
-    "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
-
-    "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
-
-    "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
-
-    "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
-
-    "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
-
-    "tldts": ["tldts@7.0.23", "", { "dependencies": { "tldts-core": "^7.0.23" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw=="],
-
-    "tldts-core": ["tldts-core@7.0.23", "", {}, "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ=="],
-
-    "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
-
-    "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
-
-    "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="],
-
-    "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
-
-    "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
-
-    "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
-
-    "ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
-
-    "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="],
-
-    "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
-
-    "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
-    "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
-
-    "tw-shimmer": ["tw-shimmer@0.4.6", "", { "peerDependencies": { "tailwindcss": ">=4.0.0-0" } }, "sha512-Wg3Qy9bcIHw6v2hqFzsvBiuIVHey2HyjDPYY/ozkDCWDYNPirxs1GoIs8FCrNtc0YTb+/wuSySAB7DjbTY6uGw=="],
-
-    "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
-
-    "type-fest": ["type-fest@5.4.4", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw=="],
-
-    "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
-
-    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
-
-    "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
-
-    "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
-
-    "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
-
-    "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
-
-    "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
-
-    "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
-
-    "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="],
-
-    "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
-
-    "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
-
-    "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="],
-
-    "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
-
-    "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
-
-    "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
-
-    "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
-
-    "unpdf": ["unpdf@1.4.0", "", { "peerDependencies": { "@napi-rs/canvas": "^0.1.69" }, "optionalPeers": ["@napi-rs/canvas"] }, "sha512-TahIk0xdH/4jh/MxfclzU79g40OyxtP00VnEUZdEkJoYtXAHWLiir6t3FC6z3vDqQTzc2ZHcla6uEiVTNjejuA=="],
-
-    "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
-
-    "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
-
-    "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
-
-    "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
-
-    "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
-
-    "use-composed-ref": ["use-composed-ref@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w=="],
-
-    "use-effect-event": ["use-effect-event@2.0.3", "", { "peerDependencies": { "react": "^18.3 || ^19.0.0-0" } }, "sha512-fz1en+z3fYXCXx3nMB8hXDMuygBltifNKZq29zDx+xNJ+1vEs6oJlYd9sK31vxJ0YI534VUsHEBY0k2BATsmBQ=="],
-
-    "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="],
-
-    "use-latest": ["use-latest@1.3.0", "", { "dependencies": { "use-isomorphic-layout-effect": "^1.1.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ=="],
-
-    "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
-
-    "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
-
-    "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
-
-    "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
-
-    "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
-
-    "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
-
-    "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
-
-    "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
-
-    "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
-
-    "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
-
-    "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
-
-    "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
-
-    "vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
-
-    "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
-
-    "vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
-
-    "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
-
-    "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
-
-    "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
-
-    "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
-
-    "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
-
-    "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
-
-    "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
-
-    "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
-
-    "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="],
-
-    "xmlbuilder": ["xmlbuilder@10.1.1", "", {}, "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg=="],
-
-    "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
-
-    "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
-
-    "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
-
-    "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
-
-    "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
-
-    "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
-
-    "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
-
-    "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
-
-    "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
-
-    "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
-
-    "zustand": ["zustand@5.0.11", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
-
-    "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
-
-    "@assistant-ui/core/assistant-stream": ["assistant-stream@0.3.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-NdtSRrQfWCDA/aqQ1xhobf/xnhuMZkhFAw9xzAt5iAoL3ouxVXOowSRN87OL4MYBQEvqtcjw9/CE6YcsXoBtuw=="],
-
-    "@assistant-ui/react/assistant-stream": ["assistant-stream@0.3.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-NdtSRrQfWCDA/aqQ1xhobf/xnhuMZkhFAw9xzAt5iAoL3ouxVXOowSRN87OL4MYBQEvqtcjw9/CE6YcsXoBtuw=="],
-
-    "@assistant-ui/react/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
-
-    "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
-
-    "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
-
-    "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
-
-    "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
-
-    "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
-
-    "@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
-
-    "@radix-ui/react-accordion/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-accordion/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-alert-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-alert-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-collapsible/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-collapsible/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-context-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-dropdown-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-form/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
-
-    "@radix-ui/react-form/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-hover-card/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-hover-card/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-menubar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-menubar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-navigation-menu/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-navigation-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-one-time-password-field/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-one-time-password-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-password-toggle-field/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-password-toggle-field/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-radio-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-radio-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-slider/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-slider/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-switch/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-switch/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-toast/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-toast/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-toggle/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-toggle-group/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-toggle-group/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-toolbar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-toolbar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
-
-    "@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
-
-    "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
-
-    "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
-
-    "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
-
-    "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
-
-    "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
-
-    "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
-
-    "@toolwind/corner-shape/@types/node": ["@types/node@20.19.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
-
-    "@ts-morph/common/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
-
-    "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
-
-    "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="],
-
-    "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
-
-    "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
-
-    "@xyflow/react/zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
-
-    "ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
-
-    "assistant-cloud/assistant-stream": ["assistant-stream@0.3.6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "nanoid": "^5.1.6", "secure-json-parse": "^4.1.0" } }, "sha512-NdtSRrQfWCDA/aqQ1xhobf/xnhuMZkhFAw9xzAt5iAoL3ouxVXOowSRN87OL4MYBQEvqtcjw9/CE6YcsXoBtuw=="],
-
-    "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
-    "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
-
-    "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
-
-    "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
-
-    "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
-
-    "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
-
-    "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
-
-    "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
-
-    "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
-
-    "langsmith/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
-
-    "langsmith/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
-
-    "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
-
-    "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
-
-    "mammoth/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
-
-    "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
-
-    "mermaid/marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
-
-    "mermaid/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="],
-
-    "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
-
-    "motion/framer-motion": ["framer-motion@12.34.3", "", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="],
-
-    "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
-
-    "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
-
-    "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
-
-    "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
-
-    "p-queue/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
-
-    "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
-
-    "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
-    "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
-
-    "radix-ui/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
-
-    "radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
-
-    "radix-ui/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
-
-    "radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
-
-    "radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
-
-    "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
-
-    "shadcn/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
-
-    "sharp/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
-
-    "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
-    "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
-
-    "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
-
-    "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
-
-    "@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
-
-    "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
-
-    "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
-
-    "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
-
-    "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="],
-
-    "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
-    "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-avatar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-collapsible/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-form/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-hover-card/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-menubar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-navigation-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-one-time-password-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-password-toggle-field/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-progress/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-radio-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-slider/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-switch/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-toast/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-toggle-group/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-toggle/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-toolbar/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "@toolwind/corner-shape/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
-
-    "@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
-
-    "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
-
-    "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
-
-    "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-    "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
-
-    "cmdk/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
-
-    "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
-
-    "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
-
-    "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
-
-    "motion/framer-motion/motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="],
-
-    "motion/framer-motion/motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
-
-    "next/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
-
-    "ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
-
-    "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-    "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
-
-    "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
-
-    "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
-  }
-}
diff --git a/studio/frontend/package.json b/studio/frontend/package.json
index 4b40759d62..d9bab4de7e 100644
--- a/studio/frontend/package.json
+++ b/studio/frontend/package.json
@@ -3,6 +3,9 @@
   "private": true,
   "version": "0.0.0",
   "type": "module",
+  "engines": {
+    "node": "^20.19.0 || >=22.12.0"
+  },
   "scripts": {
     "dev": "vite",
     "build": "tsc -b && vite build",
@@ -35,7 +38,7 @@
     "@streamdown/code": "1.0.2",
     "@streamdown/math": "1.0.2",
     "@streamdown/mermaid": "1.0.2",
-    "@tailwindcss/vite": "^4.1.18",
+    "@tailwindcss/vite": "^4.2.2",
     "@tanstack/react-router": "^1.159.10",
     "@tanstack/react-table": "^8.21.3",
     "@toolwind/corner-shape": "^0.0.8-3",
@@ -48,7 +51,6 @@
     "cmdk": "^1.1.1",
     "date-fns": "^4.1.0",
     "dexie": "^4.3.0",
-    "framer-motion": "^11.18.2",
     "js-yaml": "^4.1.1",
     "katex": "^0.16.28",
     "lucide-react": "^0.577.0",
@@ -80,13 +82,13 @@
     "@types/node": "^24.10.1",
     "@types/react": "^19.2.5",
     "@types/react-dom": "^19.2.3",
-    "@vitejs/plugin-react": "^5.1.1",
+    "@vitejs/plugin-react": "^6.0.1",
     "eslint": "^9.39.1",
     "eslint-plugin-react-hooks": "^7.0.1",
     "eslint-plugin-react-refresh": "^0.4.26",
     "globals": "^16.5.0",
     "typescript": "~5.9.3",
     "typescript-eslint": "^8.55.0",
-    "vite": "^7.3.1"
+    "vite": "^8.0.1"
   }
 }
diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx
index e5528f7ee0..0c07e133fb 100644
--- a/studio/frontend/src/components/assistant-ui/thread.tsx
+++ b/studio/frontend/src/components/assistant-ui/thread.tsx
@@ -36,7 +36,7 @@ import {
   useAuiEvent,
   useAuiState,
 } from "@assistant-ui/react";
-import { motion } from "framer-motion";
+import { motion } from "motion/react";
 import {
   ArrowDownIcon,
   ArrowUpIcon,
diff --git a/studio/setup.ps1 b/studio/setup.ps1
index 8966449423..d429958bf1 100644
--- a/studio/setup.ps1
+++ b/studio/setup.ps1
@@ -728,16 +728,22 @@ if ($IsPipInstall) {
     Write-Host "[OK] Running from pip install - frontend already bundled, skipping Node/npm check" -ForegroundColor Green
 } else {
     # setup.sh installs Node LTS (v22) via nvm. We enforce the same range here:
-    # Node >= 20, npm >= 11.
+    # Vite 8 requires Node ^20.19.0 || >=22.12.0, npm >= 11.
     $NeedNode = $true
     try {
         $NodeVersion = (node -v 2>$null)
         $NpmVersion = (npm -v 2>$null)
         if ($NodeVersion -and $NpmVersion) {
-            $NodeMajor = [int]($NodeVersion -replace 'v','').Split('.')[0]
+            $NodeParts = ($NodeVersion -replace 'v','').Split('.')
+            $NodeMajor = [int]$NodeParts[0]
+            $NodeMinor = [int]$NodeParts[1]
             $NpmMajor = [int]$NpmVersion.Split('.')[0]
 
-            if ($NodeMajor -ge 20 -and $NpmMajor -ge 11) {
+            # Vite 8: ^20.19.0 || >=22.12.0
+            $NodeOk = ($NodeMajor -eq 20 -and $NodeMinor -ge 19) -or
+                      ($NodeMajor -eq 22 -and $NodeMinor -ge 12) -or
+                      ($NodeMajor -ge 23)
+            if ($NodeOk -and $NpmMajor -ge 11) {
                 Write-Host "[OK] Node $NodeVersion and npm $NpmVersion already meet requirements." -ForegroundColor Green
                 $NeedNode = $false
             } else {
@@ -761,6 +767,24 @@ if ($IsPipInstall) {
     }
 
     Write-Host "[OK] Node $(node -v) | npm $(npm -v)" -ForegroundColor Green
+
+    # ── bun (optional, faster package installs) ──
+    # Installed via npm — Node is already guaranteed above. Works on all platforms.
+    if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
+        Write-Host "   Installing bun (faster frontend package installs)..." -ForegroundColor DarkGray
+        $prevEAP_bun = $ErrorActionPreference
+        $ErrorActionPreference = "Continue"
+        npm install -g bun 2>&1 | Out-Null
+        $ErrorActionPreference = $prevEAP_bun
+        Refresh-Environment
+        if (Get-Command bun -ErrorAction SilentlyContinue) {
+            Write-Host "[OK] bun installed ($(bun --version))" -ForegroundColor Green
+        } else {
+            Write-Host "[OK] bun install skipped (npm will be used instead)" -ForegroundColor DarkGray
+        }
+    } else {
+        Write-Host "[OK] bun already installed ($(bun --version))" -ForegroundColor Green
+    }
 }
 
 # ============================================
@@ -844,10 +868,10 @@ if ($IsPipInstall) {
             if ($NewerFile) { break }
         }
     }
-    # Also check all top-level files (package.json, bun.lock, vite.config.ts, index.html, etc.)
+    # Also check all top-level files (package.json, vite.config.ts, index.html, etc.)
     if (-not $NewerFile) {
         $NewerFile = Get-ChildItem -Path $FrontendDir -File -ErrorAction SilentlyContinue |
-            Where-Object { $_.LastWriteTime -gt $DistTime } |
+            Where-Object { $_.Name -ne "bun.lock" -and $_.LastWriteTime -gt $DistTime } |
             Select-Object -First 1
     }
     if (-not $NewerFile) {
@@ -882,26 +906,47 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
         $WalkDir = Split-Path $WalkDir -Parent
     }
 
-    # npm writes warnings to stderr; lower ErrorActionPreference so PS doesn't
-    # treat them as terminating errors (same pattern as the pip section below).
+    # Use bun if available (faster install), fall back to npm.
+    # Bun is used only as package manager; Node runs the actual build (Vite 8).
     $prevEAP_npm = $ErrorActionPreference
     $ErrorActionPreference = "Continue"
     Push-Location $FrontendDir
-    npm install 2>&1 | Out-Null
-    if ($LASTEXITCODE -ne 0) {
-        Pop-Location
-        $ErrorActionPreference = $prevEAP_npm
-        foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue }
-        Write-Host "[ERROR] npm install failed (exit code $LASTEXITCODE)" -ForegroundColor Red
-        Write-Host "   Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow
-        exit 1
+
+    $UseBun = $null -ne (Get-Command bun -ErrorAction SilentlyContinue)
+
+    if ($UseBun) {
+        Write-Host "   Using bun for package install (faster)" -ForegroundColor DarkGray
+        & bun install *> $null
+        $bunExit = $LASTEXITCODE
+        if ($bunExit -ne 0) {
+            Write-Host "   [WARN] bun install failed (exit $bunExit), falling back to npm" -ForegroundColor Yellow
+            if (Test-Path "node_modules") {
+                Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue
+            }
+            $UseBun = $false
+        }
     }
-    npm run build 2>&1 | Out-Null
-    if ($LASTEXITCODE -ne 0) {
+    if (-not $UseBun) {
+        & npm install *> $null
+        $npmExit = $LASTEXITCODE
+        if ($npmExit -ne 0) {
+            Pop-Location
+            $ErrorActionPreference = $prevEAP_npm
+            foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue }
+            Write-Host "[ERROR] npm install failed (exit code $npmExit)" -ForegroundColor Red
+            Write-Host "   Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow
+            exit 1
+        }
+    }
+
+    # Always use npm to run the build (Node runtime — avoids bun Windows runtime issues)
+    & npm run build *> $null
+    $buildExit = $LASTEXITCODE
+    if ($buildExit -ne 0) {
         Pop-Location
         $ErrorActionPreference = $prevEAP_npm
         foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue }
-        Write-Host "[ERROR] npm run build failed (exit code $LASTEXITCODE)" -ForegroundColor Red
+        Write-Host "[ERROR] npm run build failed (exit code $buildExit)" -ForegroundColor Red
         exit 1
     }
     Pop-Location
diff --git a/studio/setup.sh b/studio/setup.sh
index 851fcadc81..8f24c58023 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -69,6 +69,7 @@ _NEED_FRONTEND_BUILD=true
 if [ -d "$SCRIPT_DIR/frontend/dist" ]; then
     # Check all top-level files (package.json, bun.lock, vite.config.ts, index.html, etc.)
     _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -type f \
+        ! -name 'bun.lock' \
         -newer "$SCRIPT_DIR/frontend/dist" -print -quit 2>/dev/null)
     # Check src/ and public/ recursively (|| true guards against set -e when dirs are missing)
     if [ -z "$_changed" ]; then
@@ -85,12 +86,18 @@ else
 NEED_NODE=true
 if command -v node &>/dev/null && command -v npm &>/dev/null; then
     NODE_MAJOR=$(node -v | sed 's/v//' | cut -d. -f1)
+    NODE_MINOR=$(node -v | sed 's/v//' | cut -d. -f2)
     NPM_MAJOR=$(npm -v | cut -d. -f1)
-    if [ "$NODE_MAJOR" -ge 20 ] && [ "$NPM_MAJOR" -ge 11 ]; then
+    # Vite 8 requires Node ^20.19.0 || >=22.12.0
+    NODE_OK=false
+    if [ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -ge 19 ]; then NODE_OK=true; fi
+    if [ "$NODE_MAJOR" -eq 22 ] && [ "$NODE_MINOR" -ge 12 ]; then NODE_OK=true; fi
+    if [ "$NODE_MAJOR" -ge 23 ]; then NODE_OK=true; fi
+    if [ "$NODE_OK" = true ] && [ "$NPM_MAJOR" -ge 11 ]; then
         echo "✅ Node $(node -v) and npm $(npm -v) already meet requirements. Skipping nvm install."
         NEED_NODE=false
     else
-        if [ "$IS_COLAB" = true ]; then
+        if [ "$IS_COLAB" = true ] && [ "$NODE_OK" = true ]; then
             echo "✅ Node $(node -v) and npm $(npm -v) detected in Colab."
             # In Colab, just upgrade npm directly - nvm doesn't work well
             if [ "$NPM_MAJOR" -lt 11 ]; then
@@ -150,6 +157,20 @@ fi
 
 echo "✅ Node $(node -v) | npm $(npm -v)"
 
+# ── Install bun (optional, faster package installs) ──
+# Uses npm to install bun globally — Node is already guaranteed above,
+# avoids platform-specific installers, PATH issues, and admin requirements.
+if ! command -v bun &>/dev/null; then
+    echo "   Installing bun (faster frontend package installs)..."
+    if npm install -g bun > /dev/null 2>&1 && command -v bun &>/dev/null; then
+        echo "✅ bun installed ($(bun --version))"
+    else
+        echo "   bun install skipped (npm will be used instead)"
+    fi
+else
+    echo "✅ bun already installed ($(bun --version))"
+fi
+
 # ── 5. Build frontend ──
 cd "$SCRIPT_DIR/frontend"
 
@@ -174,7 +195,27 @@ _restore_gitignores() {
 }
 trap _restore_gitignores EXIT
 
-run_quiet "npm install" npm install
+# Use bun for install if available (faster), fall back to npm.
+# Build always uses npm (Node runtime — avoids bun runtime issues on some platforms).
+# NOTE: We intentionally avoid run_quiet for the bun install attempt because
+# run_quiet calls exit on failure, which would kill the script before the npm
+# fallback can run. Instead we capture output manually and only show it on failure.
+if command -v bun &>/dev/null; then
+    echo "   Using bun for package install (faster)"
+    _bun_log=$(mktemp)
+    if bun install >"$_bun_log" 2>&1; then
+        rm -f "$_bun_log"
+    else
+        echo "   ⚠️  bun install failed, falling back to npm"
+        echo "   bun install output:"
+        sed 's/^/   | /' "$_bun_log" >&2
+        rm -f "$_bun_log"
+        rm -rf node_modules
+        run_quiet "npm install" npm install
+    fi
+else
+    run_quiet "npm install" npm install
+fi
 run_quiet "npm run build" npm run build
 
 _restore_gitignores

From 7eb48512bce1046f926da8c64b4615509703f940 Mon Sep 17 00:00:00 2001
From: cz-03 
Date: Wed, 25 Mar 2026 13:29:01 +0200
Subject: [PATCH 20/34] feat(tokenizer): add get_tokenizer_info() diagnostic
 helper (#4436)

* feat(tokenizer): add get_tokenizer_info() diagnostic helper

Adds get_tokenizer_info(tokenizer) to tokenizer_utils.py returning a concise dict of key tokenizer properties class name, is_fast, vocab size, added token count, model_max_length, padding side, special tokens (bos, eos, pad, unk), chat template presence, and total special token count. All fields use getattr(..., None) fallbacks so the function never raises on unusual or partially initialized tokenizers. Exported via __all__ alongside the existing public helpers. Useful for logging, debugging, and surfacing tokenizer state in the Unsloth Studio UI.

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

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

* Fix docstring, remove artifact, restore valuable comments in tokenizer_utils.py

- Fix get_tokenizer_info() docstring example: correct tokenizer_class to
  PreTrainedTokenizerFast, vocab_size to 128000, swap added_tokens_count (256)
  and special_tokens_count (3) to match actual Llama-3.2-1B-Instruct output
- Remove accidentally committed "# ... (rest of file unchanged)" diff artifact
- Restore fix_sentencepiece_gguf() docstring with llama.cpp upstream link
- Restore 10 comments containing upstream URLs, model-specific workarounds,
  and non-obvious context (issue #292, sentencepiece#121, Starling hack,
  Kaggle /tmp limit, Deepseek slow tokenizer, twitter/danielhanchen references)

* Revert "Fix docstring, remove artifact, restore valuable comments in tokenizer_utils.py"

This reverts commit 4e525b734b95e56ab18229c4f0fd4fb97cd1f01a.

* Revert all deletions, keep only get_tokenizer_info() addition

Restore tokenizer_utils.py to main and add only the new
get_tokenizer_info() function and its __all__ entry.
All comment removals, dead code cleanup, and formatting
changes from the original PR are reverted.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han 
---
 unsloth/tokenizer_utils.py | 50 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/unsloth/tokenizer_utils.py b/unsloth/tokenizer_utils.py
index 96c22f62ff..8be6bb5a5a 100644
--- a/unsloth/tokenizer_utils.py
+++ b/unsloth/tokenizer_utils.py
@@ -42,6 +42,7 @@ __all__ = [
     "check_tokenizer",
     "add_new_tokens",
     "fix_sentencepiece_gguf",
+    "get_tokenizer_info",
 ]
 
 
@@ -896,6 +897,55 @@ def check_tokenizer(
     return convert_to_fast_tokenizer(tokenizer)
 
 
+def get_tokenizer_info(tokenizer) -> dict:
+    """Return a concise diagnostic summary of a tokenizer instance.
+
+    Collects key properties into a plain dict suitable for logging, debugging,
+    or displaying in the Unsloth Studio UI. All fields are safe to access —
+    missing attributes fall back to ``None`` rather than raising.
+
+    Example output::
+
+        {
+            "name_or_path": "unsloth/Llama-3.2-1B-Instruct",
+            "tokenizer_class": "PreTrainedTokenizerFast",
+            "is_fast": True,
+            "vocab_size": 128000,
+            "added_tokens_count": 256,
+            "model_max_length": 131072,
+            "padding_side": "right",
+            "bos_token": "<|begin_of_text|>",
+            "eos_token": "<|eot_id|>",
+            "pad_token": "<|finetune_right_pad_id|>",
+            "unk_token": None,
+            "has_chat_template": True,
+            "special_tokens_count": 3,
+        }
+
+    Args:
+        tokenizer: Any HuggingFace ``PreTrainedTokenizer`` or
+                   ``PreTrainedTokenizerFast`` instance.
+
+    Returns:
+        A ``dict`` of tokenizer properties. Safe to serialize to JSON.
+    """
+    return {
+        "name_or_path": getattr(tokenizer, "name_or_path", None),
+        "tokenizer_class": type(tokenizer).__name__,
+        "is_fast": getattr(tokenizer, "is_fast", False),
+        "vocab_size": getattr(tokenizer, "vocab_size", None),
+        "added_tokens_count": len(getattr(tokenizer, "added_tokens_decoder", {})),
+        "model_max_length": getattr(tokenizer, "model_max_length", None),
+        "padding_side": getattr(tokenizer, "padding_side", None),
+        "bos_token": getattr(tokenizer, "bos_token", None),
+        "eos_token": getattr(tokenizer, "eos_token", None),
+        "pad_token": getattr(tokenizer, "pad_token", None),
+        "unk_token": getattr(tokenizer, "unk_token", None),
+        "has_chat_template": getattr(tokenizer, "chat_template", None) is not None,
+        "special_tokens_count": len(getattr(tokenizer, "all_special_tokens", [])),
+    }
+
+
 import inspect
 from inspect import getsource
 import trl

From 3446e0c489cc6c24f33ff26fcf57a892de848a89 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 04:50:23 -0700
Subject: [PATCH 21/34] Add ROCm (AMD GPU) support to studio setup (#4585)

* Add support for ROCm in studio setup

* Fix ROCm detection bugs: ROCM_PATH resolution, CUDA guard, compiler selection

- Set GPU_BACKEND="cuda" when nvcc is found (CUDA path was unreachable)
- Guard ROCm detection with `if [ -z "$GPU_BACKEND" ]` so CUDA takes
  priority on mixed-toolchain hosts
- Rename ROCM_PATH to ROCM_HIPCC for the hipcc binary; resolve the
  actual ROCm root via readlink -f and hipconfig -R into ROCM_ROOT
- Export both ROCM_PATH and HIP_PATH as the resolved root directory
- Use HIPCXX via hipconfig -l instead of legacy CMAKE_C_COMPILER=hipcc
- Switch grep -oP to grep -oE for portability across Linux distros
- Use GPU_TARGETS (upstream cmake variable) instead of AMDGPU_TARGETS
- Remove stale hardcoded fallback targets; let cmake auto-detect instead

* Fix gfx regex to match gfx90a (MI210/MI250/MI250X)

The grep and bash regex used {3,4} digits after 'gfx', which silently
excluded gfx90a (2 digits + letter 'a') -- the architecture for AMD
Instinct MI210, MI250, and MI250X data-center GPUs. Change to {2,4}
so all real gfx targets from gfx90a through gfx1200 are matched.

---------

Co-authored-by: edamamez 
---
 studio/setup.sh | 69 ++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 68 insertions(+), 1 deletion(-)

diff --git a/studio/setup.sh b/studio/setup.sh
index 8f24c58023..97bfbbfadc 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -490,17 +490,40 @@ rm -rf "$LLAMA_CPP_DIR"
                 echo "   Using ccache for faster compilation"
             fi
 
-            # Detect CUDA: check nvcc on PATH, then common install locations
+            # Detect GPU backend: CUDA (NVIDIA) or ROCm (AMD)
+            GPU_BACKEND=""
+
+            # Check for CUDA: check nvcc on PATH, then common install locations
             NVCC_PATH=""
             if command -v nvcc &>/dev/null; then
                 NVCC_PATH="$(command -v nvcc)"
+                GPU_BACKEND="cuda"
             elif [ -x /usr/local/cuda/bin/nvcc ]; then
                 NVCC_PATH="/usr/local/cuda/bin/nvcc"
                 export PATH="/usr/local/cuda/bin:$PATH"
+                GPU_BACKEND="cuda"
             elif ls /usr/local/cuda-*/bin/nvcc &>/dev/null 2>&1; then
                 # Pick the newest cuda-XX.X directory
                 NVCC_PATH="$(ls -d /usr/local/cuda-*/bin/nvcc 2>/dev/null | sort -V | tail -1)"
                 export PATH="$(dirname "$NVCC_PATH"):$PATH"
+                GPU_BACKEND="cuda"
+            fi
+
+            # Check for ROCm (AMD) only if CUDA was not already selected
+            ROCM_HIPCC=""
+            if [ -z "$GPU_BACKEND" ]; then
+                if command -v hipcc &>/dev/null; then
+                    ROCM_HIPCC="$(command -v hipcc)"
+                    GPU_BACKEND="rocm"
+                elif [ -x /opt/rocm/bin/hipcc ]; then
+                    ROCM_HIPCC="/opt/rocm/bin/hipcc"
+                    export PATH="/opt/rocm/bin:$PATH"
+                    GPU_BACKEND="rocm"
+                elif ls /opt/rocm-*/bin/hipcc &>/dev/null 2>&1; then
+                    ROCM_HIPCC="$(ls -d /opt/rocm-*/bin/hipcc 2>/dev/null | sort -V | tail -1)"
+                    export PATH="$(dirname "$ROCM_HIPCC"):$PATH"
+                    GPU_BACKEND="rocm"
+                fi
             fi
 
             if [ -n "$NVCC_PATH" ]; then
@@ -535,9 +558,53 @@ rm -rf "$LLAMA_CPP_DIR"
 
                 # Multi-threaded nvcc compilation (uses all CPU cores per .cu file)
                 CMAKE_ARGS="$CMAKE_ARGS -DCMAKE_CUDA_FLAGS=--threads=0"
+            elif [ "$GPU_BACKEND" = "rocm" ]; then
+                # Resolve hipcc symlinks to find the real ROCm root
+                _HIPCC_REAL="$(readlink -f "$ROCM_HIPCC" 2>/dev/null || printf '%s' "$ROCM_HIPCC")"
+                ROCM_ROOT=""
+                if command -v hipconfig &>/dev/null; then
+                    ROCM_ROOT="$(hipconfig -R 2>/dev/null || true)"
+                fi
+                if [ -z "$ROCM_ROOT" ]; then
+                    ROCM_ROOT="$(cd "$(dirname "$_HIPCC_REAL")/.." 2>/dev/null && pwd)"
+                fi
+
+                echo "   Building with ROCm support (AMD GPU, hipcc: $_HIPCC_REAL)..."
+                CMAKE_ARGS="$CMAKE_ARGS -DGGML_HIP=ON"
+                export ROCM_PATH="$ROCM_ROOT"
+                export HIP_PATH="$ROCM_ROOT"
+
+                # Use upstream-recommended HIP compiler (not legacy hipcc-as-CXX)
+                if command -v hipconfig &>/dev/null; then
+                    _HIP_CLANG_DIR="$(hipconfig -l 2>/dev/null || true)"
+                    [ -n "$_HIP_CLANG_DIR" ] && export HIPCXX="$_HIP_CLANG_DIR/clang"
+                fi
+
+                # Detect AMD GPU architecture (gfx target)
+                GPU_TARGETS=""
+                if command -v rocminfo &>/dev/null; then
+                    _gfx_list=$(rocminfo 2>/dev/null | grep -oE 'gfx[0-9]{2,4}[a-z]?' | sort -u || true)
+                    _valid_gfx=""
+                    for _gfx in $_gfx_list; do
+                        if [[ "$_gfx" =~ ^gfx[0-9]{2,4}[a-z]?$ ]]; then
+                            _valid_gfx="${_valid_gfx}${_valid_gfx:+;}$_gfx"
+                        fi
+                    done
+                    [ -n "$_valid_gfx" ] && GPU_TARGETS="$_valid_gfx"
+                fi
+
+                if [ -n "$GPU_TARGETS" ]; then
+                    echo "   AMD GPU architectures: ${GPU_TARGETS//;/, } -- limiting build to detected targets"
+                    CMAKE_ARGS="$CMAKE_ARGS -DGPU_TARGETS=${GPU_TARGETS}"
+                else
+                    echo "   Could not detect AMD GPU arch -- building for default targets (cmake will auto-detect)"
+                fi
             elif [ -d /usr/local/cuda ] || nvidia-smi &>/dev/null; then
                 echo "   CUDA driver detected but nvcc not found — building CPU-only"
                 echo "   To enable GPU: install cuda-toolkit or add nvcc to PATH"
+            elif [ -d /opt/rocm ] || command -v rocm-smi &>/dev/null; then
+                echo "   ROCm driver detected but hipcc not found — building CPU-only"
+                echo "   To enable GPU: install rocm-dev or add hipcc to PATH"
             else
                 echo "   Building CPU-only (no CUDA detected)..."
             fi

From 19e9c60a8e5482618e6c81f83cfae891af990e66 Mon Sep 17 00:00:00 2001
From: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
Date: Wed, 25 Mar 2026 16:24:21 +0400
Subject: [PATCH 22/34] Consolidate dual venvs and separate install from update
 (#4530)

* refactor: consolidate dual venvs into single ~/.unsloth/studio/unsloth_studio

* refactor: separate install.sh (first-time) from setup.sh (smart update with PyPI version check)

* fix: install.sh calls setup.sh directly, keep both setup and update CLI commands

* fix: use importlib.resources.files() directly without _path attribute

* fix: bootstrap uv before pip upgrade to handle uv venvs without pip

* fix: frontend 404 when launched via CLI, add global symlink to ~/.local/bin

* feat: add --local flag to install.sh and unsloth studio update for branch testing

* fix: resolve repo root from script location for --local installs

* feat: add --package flag to install.sh for testing with custom package names

* feat: add --package flag to unsloth studio update

* fix: always nuke venv in install.sh for clean installs

* revert: remove Windows changes, will handle in separate PR

* fix: error when --package is passed without an argument

* revert: restore Windows scripts to current main

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

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

* fix: always explicitly set STUDIO_LOCAL_INSTALL and STUDIO_PACKAGE_NAME env vars

* fix: pass explicit STUDIO_LOCAL_REPO env var for --local installs

* fix: align banner box for Setup vs Update labels

* deprecate: hide 'unsloth studio setup' command, point users to update/install.sh

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

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

* fix: check stdout not stdin for auto-launch detection (curl pipe fix)

* fix: update install URL to unsloth.ai/install.sh

* fix: update install.sh usage comments to unsloth.ai/install.sh

* fix: use --upgrade-package for base deps to preserve existing torch/CUDA installs

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

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

* fix: --local install now also installs unsloth-zoo via base.txt before editable overlay

* fix: don't skip base packages for --local installs (editable needs unsloth-zoo)

* refactor: move --local full dep install to install.sh, keep SKIP_STUDIO_BASE for all paths

* feat: add migration support for old .venv and CWD-based installs in setup.sh

* Revert "feat: add migration support for old .venv and CWD-based installs in setup.sh"

This reverts commit 301291d0028b61e15acc064829f48be50c764087.

* feat: migrate old .venv layout in install.sh instead of always nuking

* feat: validate old .venv with torch CUDA test before migration, recovery message on launch failure

* fix: try CUDA then fall back to CPU for migration validation

* fix: upgrade unsloth/unsloth-zoo with --reinstall-package on migration to preserve torch

* remove: delete unused unsloth ui command (use unsloth studio instead)

* Fix Windows venv path mismatch between install.ps1, setup.ps1, and studio.py

install.ps1 was creating the venv CWD-relative ($VenvName = "unsloth_studio"),
setup.ps1 was using an absolute path to ".unsloth\studio\.venv", and studio.py
looks for ".unsloth\studio\unsloth_studio". All three paths were different, so
the Windows installer would never produce a working Studio setup.

install.ps1:
- Use absolute $StudioHome + $VenvDir matching the Linux install.sh layout
- Add 3-way migration: old .venv at STUDIO_HOME, CWD-relative ~/unsloth_studio
  from the previous install.ps1, or fresh creation with torch validation
- For migrated envs, upgrade unsloth while preserving existing torch/CUDA wheels
- Set SKIP_STUDIO_BASE=1 before calling setup.ps1 (matches install.sh behavior)
- Fix launch instructions to use the absolute venv path

setup.ps1:
- Change $VenvDir from ".unsloth\studio\.venv" to ".unsloth\studio\unsloth_studio"
- Add SKIP_STUDIO_BASE guard: error out if venv is missing when called from
  install.ps1 (which should have already created it)
- Differentiate "Setup" vs "Update" in banners based on SKIP_STUDIO_BASE

* setup.ps1: unconditionally error if venv missing, matching setup.sh

setup.sh always errors out if the venv does not exist (line 224-228),
telling the user to run install.sh first. setup.ps1 was conditionally
creating a bare venv with python -m venv when SKIP_STUDIO_BASE was not
set, which would produce an empty venv with no torch or unsloth. Now
setup.ps1 matches setup.sh: always error, always point to install.ps1.

* Fix --torch-backend=auto CPU solver dead-end on Linux, macOS, and Windows

On CPU-only machines, `uv pip install unsloth --torch-backend=auto`
falls back to unsloth==2024.8 because the CPU solver cannot satisfy
newer unsloth's dependencies. install.ps1 already solved this with a
two-step approach; this applies the same fix to install.sh and
install_python_stack.py.

install.sh: add get_torch_index_url() that detects GPU via nvidia-smi
and maps CUDA versions to PyTorch index URLs (matching install.ps1's
Get-TorchIndexUrl). Fresh installs now install torch first via explicit
--index-url, then install unsloth with --upgrade-package to preserve
the pre-installed torch. All 5 --torch-backend=auto removed from
primary paths.

install.ps1: add fallback else-branch when TorchIndexUrl is empty,
using --torch-backend=auto as last resort (matching install.sh).

install_python_stack.py: remove unconditional --torch-backend=auto
from _build_uv_cmd. Torch is pre-installed by install.sh/setup.ps1
by the time this runs. Callers that need it can set UV_TORCH_BACKEND.

Both install.sh and install.ps1 now share the same three-branch logic:
migrated env (upgrade-package only), normal (torch-first + index-url),
and fallback (--torch-backend=auto if URL detection fails).

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

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

* Use --reinstall-package for migrated envs on both Linux and Windows

For migrated environments (moved from legacy venv location),
--reinstall-package is better than --upgrade-package because it forces
a clean reinstall even if the same version is already installed. This
ensures proper .dist-info and .pyc state in the new venv location.

--upgrade-package remains correct for the fresh install path where
torch is already installed and we just want to add unsloth without
re-resolving torch.

* Address review findings: portability, parity, and stale comments

- Replace grep -oP (GNU Perl regex) with POSIX sed in
  get_torch_index_url() so the script works on BSD grep (macOS is
  already guarded by the Darwin early-return, but Alpine/BusyBox
  would silently get the wrong CUDA tag)
- Add LC_ALL=C before nvidia-smi invocation to prevent locale-dependent
  output parsing issues
- Add warning on stderr when nvidia-smi output is unparseable, matching
  install.ps1's [WARN] message
- Add explicit unsloth-zoo positional arg to install.ps1 migrated path,
  matching install.sh (--reinstall-package alone won't install it if it
  was never present in the migrated env)
- Fix stale comment in install_python_stack.py line 392 that still
  claimed --torch-backend=auto is added by _build_uv_cmd
- Add sed to test tools directory (function now uses sed instead of grep)

* Add --index-url to migrated env path to prevent CPU torch resolution

The migrated path runs uv pip install with --reinstall-package for
unsloth/unsloth-zoo. While uv should keep existing torch as satisfied,
the resolver could still re-resolve torch as a transitive dependency.
Without --index-url pointing at the correct CUDA wheel index, the
resolver would fall back to plain PyPI and potentially pull CPU-only
torch. Adding --index-url $TORCH_INDEX_URL ensures CUDA wheels are
available if the resolver needs them.

Applied to both install.sh and install.ps1.

* Revert --index-url on migrated env path

The original install.ps1 on main already handles the migrated path
without --index-url and it works correctly. --reinstall-package only
forces reinstall of the named packages while uv keeps existing torch
as satisfied. No need for the extra flag.

* Fix unsloth studio update --local not installing local checkout

studio.py sets STUDIO_LOCAL_REPO when --local is passed, but
install_python_stack.py never read it. The update path always
installed from PyPI regardless of the --local flag.

Add a local_repo branch that first updates deps from base.txt
(with --upgrade-package to preserve torch), then overlays the
local checkout as an editable install with --no-deps.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han 
---
 install.ps1                                |  88 +++++--
 install.sh                                 | 261 +++++++++++++++++++--
 studio/backend/colab.py                    |   2 +-
 studio/install_python_stack.py             | 114 +++++++--
 studio/setup.ps1                           |  27 ++-
 studio/setup.sh                            | 196 ++++++----------
 tests/python/__init__.py                   |   0
 tests/python/test_cross_platform_parity.py | 137 +++++++++++
 tests/python/test_install_python_stack.py  |  56 +++++
 tests/run_all.sh                           |  16 ++
 tests/sh/test_get_torch_index_url.sh       | 128 ++++++++++
 unsloth_cli/__init__.py                    |   2 -
 unsloth_cli/commands/studio.py             |  48 +++-
 unsloth_cli/commands/ui.py                 | 103 --------
 14 files changed, 877 insertions(+), 301 deletions(-)
 create mode 100644 tests/python/__init__.py
 create mode 100644 tests/python/test_cross_platform_parity.py
 create mode 100644 tests/python/test_install_python_stack.py
 create mode 100755 tests/run_all.sh
 create mode 100755 tests/sh/test_get_torch_index_url.sh
 delete mode 100644 unsloth_cli/commands/ui.py

diff --git a/install.ps1 b/install.ps1
index 1613ec6258..83576d9c75 100644
--- a/install.ps1
+++ b/install.ps1
@@ -5,8 +5,9 @@
 function Install-UnslothStudio {
     $ErrorActionPreference = "Stop"
 
-    $VenvName = "unsloth_studio"
     $PythonVersion = "3.13"
+    $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio"
+    $VenvDir = Join-Path $StudioHome "unsloth_studio"
 
     Write-Host ""
     Write-Host "========================================="
@@ -449,20 +450,59 @@ shell.Run cmd, 0, False
         return
     }
 
-    # ── Create venv (skip if it already exists and has a valid interpreter) ──
+    # ── Create venv (migrate old layout if possible, otherwise fresh) ──
     # Pass the resolved executable path to uv so it does not re-resolve
     # a version string back to a conda interpreter.
-    $VenvPython = Join-Path $VenvName "Scripts\python.exe"
+    if (-not (Test-Path $StudioHome)) {
+        New-Item -ItemType Directory -Path $StudioHome -Force | Out-Null
+    }
+
+    $VenvPython = Join-Path $VenvDir "Scripts\python.exe"
+    $_Migrated = $false
+
+    if (Test-Path $VenvPython) {
+        # New layout already exists -- nuke for fresh install
+        Write-Host "==> Removing existing environment for fresh install..."
+        Remove-Item -Recurse -Force $VenvDir
+    } elseif (Test-Path (Join-Path $StudioHome ".venv\Scripts\python.exe")) {
+        # Old layout (~/.unsloth/studio/.venv) exists -- validate before migrating
+        $OldVenv = Join-Path $StudioHome ".venv"
+        $OldPy = Join-Path $OldVenv "Scripts\python.exe"
+        Write-Host "==> Found legacy Studio environment, validating..."
+        $prevEAP2 = $ErrorActionPreference
+        $ErrorActionPreference = "Continue"
+        try {
+            & $OldPy -c "import torch; A = torch.ones((2,2)); B = A + A" 2>$null | Out-Null
+            $torchOk = ($LASTEXITCODE -eq 0)
+        } catch { $torchOk = $false }
+        $ErrorActionPreference = $prevEAP2
+        if ($torchOk) {
+            Write-Host "   Legacy environment is healthy -- migrating..."
+            Move-Item -Path $OldVenv -Destination $VenvDir -Force
+            Write-Host "   Moved .venv -> unsloth_studio"
+            $_Migrated = $true
+        } else {
+            Write-Host "   Legacy environment failed validation -- creating fresh environment"
+            Remove-Item -Recurse -Force $OldVenv -ErrorAction SilentlyContinue
+        }
+    } elseif (Test-Path (Join-Path $env:USERPROFILE "unsloth_studio\Scripts\python.exe")) {
+        # CWD-relative venv from old install.ps1 -- migrate to absolute path
+        $CwdVenv = Join-Path $env:USERPROFILE "unsloth_studio"
+        Write-Host "==> Found CWD-relative Studio environment, migrating to $VenvDir..."
+        Move-Item -Path $CwdVenv -Destination $VenvDir -Force
+        Write-Host "   Moved ~/unsloth_studio -> ~/.unsloth/studio/unsloth_studio"
+        $_Migrated = $true
+    }
+
     if (-not (Test-Path $VenvPython)) {
-        if (Test-Path $VenvName) { Remove-Item -Recurse -Force $VenvName }
-        Write-Host "==> Creating Python $($DetectedPython.Version) virtual environment (${VenvName})..."
-        uv venv $VenvName --python "$($DetectedPython.Path)"
+        Write-Host "==> Creating Python $($DetectedPython.Version) virtual environment ($VenvDir)..."
+        uv venv $VenvDir --python "$($DetectedPython.Path)"
         if ($LASTEXITCODE -ne 0) {
             Write-Host "[ERROR] Failed to create virtual environment (exit code $LASTEXITCODE)" -ForegroundColor Red
             return
         }
     } else {
-        Write-Host "==> Virtual environment ${VenvName} already exists, skipping creation."
+        Write-Host "==> Using migrated environment at $VenvDir"
     }
 
     # ── Detect GPU (robust: PATH + hardcoded fallback paths, mirrors setup.ps1) ──
@@ -536,15 +576,26 @@ shell.Run cmd, 0, False
     #   CUDA wheels.  Missing dependencies (transformers, trl, peft, etc.)
     #   are still pulled in because they are new, not upgrades.
     #
-    Write-Host "==> Installing PyTorch ($TorchIndexUrl)..."
-    uv pip install --python $VenvPython torch torchvision torchaudio --index-url $TorchIndexUrl
-    if ($LASTEXITCODE -ne 0) {
-        Write-Host "[ERROR] Failed to install PyTorch (exit code $LASTEXITCODE)" -ForegroundColor Red
-        return
-    }
+    if ($_Migrated) {
+        # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state
+        # in the new venv location, while preserving existing torch/CUDA
+        Write-Host "==> Upgrading unsloth in migrated environment..."
+        uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.11" unsloth-zoo
+    } elseif ($TorchIndexUrl) {
+        Write-Host "==> Installing PyTorch ($TorchIndexUrl)..."
+        uv pip install --python $VenvPython torch torchvision torchaudio --index-url $TorchIndexUrl
+        if ($LASTEXITCODE -ne 0) {
+            Write-Host "[ERROR] Failed to install PyTorch (exit code $LASTEXITCODE)" -ForegroundColor Red
+            return
+        }
 
-    Write-Host "==> Installing unsloth (this may take a few minutes)..."
-    uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.3.11"
+        Write-Host "==> Installing unsloth (this may take a few minutes)..."
+        uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.3.11"
+    } else {
+        # Fallback: GPU detection failed to produce a URL -- let uv resolve torch
+        Write-Host "==> Installing unsloth (this may take a few minutes)..."
+        uv pip install --python $VenvPython "unsloth>=2026.3.11" --torch-backend=auto
+    }
     if ($LASTEXITCODE -ne 0) {
         Write-Host "[ERROR] Failed to install unsloth (exit code $LASTEXITCODE)" -ForegroundColor Red
         return
@@ -554,7 +605,7 @@ shell.Run cmd, 0, False
     # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools,
     # CUDA Toolkit, Node.js, and other dependencies automatically via winget.
     Write-Host "==> Running unsloth studio setup..."
-    $UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe"
+    $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe"
     if (-not (Test-Path $UnslothExe)) {
         Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red
         Write-Host "        Expected: $UnslothExe" -ForegroundColor Yellow
@@ -562,6 +613,8 @@ shell.Run cmd, 0, False
         Write-Host "        Try re-running the installer or see: https://github.com/unslothai/unsloth?tab=readme-ov-file#-quickstart" -ForegroundColor Yellow
         return
     }
+    # Tell setup.ps1 to skip base package installation (install.ps1 already did it)
+    $env:SKIP_STUDIO_BASE = "1"
     & $UnslothExe studio setup
     if ($LASTEXITCODE -ne 0) {
         Write-Host "[ERROR] unsloth studio setup failed (exit code $LASTEXITCODE)" -ForegroundColor Red
@@ -582,12 +635,11 @@ shell.Run cmd, 0, False
     if ($IsInteractive) {
         Write-Host "==> Launching Unsloth Studio..."
         Write-Host ""
-        $UnslothExe = Join-Path $VenvName "Scripts\unsloth.exe"
         & $UnslothExe studio -H 0.0.0.0 -p 8888
     } else {
         Write-Host "  To launch, run:"
         Write-Host ""
-        Write-Host "    .\${VenvName}\Scripts\activate"
+        Write-Host "    & `"$VenvDir\Scripts\Activate.ps1`""
         Write-Host "    unsloth studio -H 0.0.0.0 -p 8888"
         Write-Host ""
     }
diff --git a/install.sh b/install.sh
index 0893955939..ec5008af12 100755
--- a/install.sh
+++ b/install.sh
@@ -1,11 +1,35 @@
 #!/bin/sh
 # Unsloth Studio Installer
-# Usage (curl): curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh
-# Usage (wget): wget -qO- https://raw.githubusercontent.com/unslothai/unsloth/main/install.sh | sh
+# Usage (curl):  curl -fsSL https://unsloth.ai/install.sh | sh
+# Usage (wget):  wget -qO- https://unsloth.ai/install.sh | sh
+# Usage (local): ./install.sh --local   (install from local repo instead of PyPI)
+# Usage (test):  ./install.sh --package roland-sloth  (install a different package name)
 set -e
 
-VENV_NAME="unsloth_studio"
+# ── Parse flags ──
+STUDIO_LOCAL_INSTALL=false
+PACKAGE_NAME="unsloth"
+_next_is_package=false
+for arg in "$@"; do
+    if [ "$_next_is_package" = true ]; then
+        PACKAGE_NAME="$arg"
+        _next_is_package=false
+        continue
+    fi
+    case "$arg" in
+        --local) STUDIO_LOCAL_INSTALL=true ;;
+        --package) _next_is_package=true ;;
+    esac
+done
+
+if [ "$_next_is_package" = true ]; then
+    echo "❌ ERROR: --package requires an argument." >&2
+    exit 1
+fi
+
 PYTHON_VERSION="3.13"
+STUDIO_HOME="$HOME/.unsloth/studio"
+VENV_DIR="$STUDIO_HOME/unsloth_studio"
 
 # ── Helper: download a URL to a file (supports curl and wget) ──
 download() {
@@ -659,32 +683,195 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then
     export PATH="$HOME/.local/bin:$PATH"
 fi
 
-# ── Create venv (skip if it already exists and has a valid interpreter) ──
-if [ ! -x "$VENV_NAME/bin/python" ]; then
-    [ -e "$VENV_NAME" ] && rm -rf "$VENV_NAME"
-    echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_NAME})..."
-    uv venv "$VENV_NAME" --python "$PYTHON_VERSION"
-else
-    echo "==> Virtual environment ${VENV_NAME} already exists, skipping creation."
+# ── Create venv (migrate old layout if possible, otherwise fresh) ──
+mkdir -p "$STUDIO_HOME"
+
+_MIGRATED=false
+
+if [ -x "$VENV_DIR/bin/python" ]; then
+    # New layout already exists — nuke for fresh install
+    rm -rf "$VENV_DIR"
+elif [ -x "$STUDIO_HOME/.venv/bin/python" ]; then
+    # Old layout exists — validate before migrating
+    echo "==> Found legacy Studio environment, validating..."
+    if "$STUDIO_HOME/.venv/bin/python" -c "
+import torch
+device = 'cuda' if torch.cuda.is_available() else 'cpu'
+A = torch.ones((10, 10), device=device)
+B = torch.ones((10, 10), device=device)
+C = torch.ones((10, 10), device=device)
+D = A + B
+E = D @ C
+torch.testing.assert_close(torch.unique(E), torch.tensor((20,), device=E.device, dtype=E.dtype))
+" >/dev/null 2>&1; then
+        echo "✅ Legacy environment is healthy — migrating..."
+        mv "$STUDIO_HOME/.venv" "$VENV_DIR"
+        echo "   Moved ~/.unsloth/studio/.venv → $VENV_DIR"
+        _MIGRATED=true
+    else
+        echo "⚠️  Legacy environment failed validation — creating fresh environment"
+        rm -rf "$STUDIO_HOME/.venv"
+    fi
 fi
 
+if [ ! -x "$VENV_DIR/bin/python" ]; then
+    echo "==> Creating Python ${PYTHON_VERSION} virtual environment (${VENV_DIR})..."
+    uv venv "$VENV_DIR" --python "$PYTHON_VERSION"
+else
+    echo "==> Using migrated environment at ${VENV_DIR}"
+fi
+
+# ── Resolve repo root (for --local installs) ──
+_REPO_ROOT="$(cd "$(dirname "$0" 2>/dev/null || echo ".")" && pwd)"
+
+# ── Detect GPU and choose PyTorch index URL ──
+# Mirrors Get-TorchIndexUrl in install.ps1.
+# On CPU-only machines this returns the cpu index, avoiding the solver
+# dead-end where --torch-backend=auto resolves to unsloth==2024.8.
+get_torch_index_url() {
+    _base="https://download.pytorch.org/whl"
+    # macOS: always CPU (no CUDA support)
+    case "$(uname -s)" in Darwin) echo "$_base/cpu"; return ;; esac
+    # Try nvidia-smi
+    _smi=""
+    if command -v nvidia-smi >/dev/null 2>&1; then
+        _smi="nvidia-smi"
+    elif [ -x "/usr/bin/nvidia-smi" ]; then
+        _smi="/usr/bin/nvidia-smi"
+    fi
+    if [ -z "$_smi" ]; then echo "$_base/cpu"; return; fi
+    # Parse CUDA version from nvidia-smi output (POSIX-safe, no grep -P)
+    _cuda_ver=$(LC_ALL=C $_smi 2>/dev/null \
+        | sed -n 's/.*CUDA Version:[[:space:]]*\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' \
+        | head -1)
+    if [ -z "$_cuda_ver" ]; then
+        echo "[WARN] Could not determine CUDA version from nvidia-smi, defaulting to cu126" >&2
+        echo "$_base/cu126"; return
+    fi
+    _major=${_cuda_ver%%.*}
+    _minor=${_cuda_ver#*.}
+    if [ "$_major" -ge 13 ]; then echo "$_base/cu130"
+    elif [ "$_major" -eq 12 ] && [ "$_minor" -ge 8 ]; then echo "$_base/cu128"
+    elif [ "$_major" -eq 12 ] && [ "$_minor" -ge 6 ]; then echo "$_base/cu126"
+    elif [ "$_major" -ge 12 ]; then echo "$_base/cu124"
+    elif [ "$_major" -ge 11 ]; then echo "$_base/cu118"
+    else echo "$_base/cpu"; fi
+}
+TORCH_INDEX_URL=$(get_torch_index_url)
+
 # ── Install unsloth directly into the venv (no activation needed) ──
-echo "==> Installing unsloth (this may take a few minutes)..."
-uv pip install --python "$VENV_NAME/bin/python" "unsloth>=2026.3.11" --torch-backend=auto
+_VENV_PY="$VENV_DIR/bin/python"
+if [ "$_MIGRATED" = true ]; then
+    # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state
+    # in the new venv location, while preserving existing torch/CUDA
+    echo "==> Upgrading unsloth in migrated environment..."
+    uv pip install --python "$_VENV_PY" \
+        --reinstall-package unsloth --reinstall-package unsloth-zoo \
+        "unsloth>=2026.3.11" unsloth-zoo
+    if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
+        echo "==> Overlaying local repo (editable)..."
+        uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps
+    fi
+elif [ -n "$TORCH_INDEX_URL" ]; then
+    # Fresh: Step 1 - install torch from explicit index
+    echo "==> Installing PyTorch ($TORCH_INDEX_URL)..."
+    uv pip install --python "$_VENV_PY" torch torchvision torchaudio \
+        --index-url "$TORCH_INDEX_URL"
+    # Fresh: Step 2 - install unsloth, preserving pre-installed torch
+    echo "==> Installing unsloth (this may take a few minutes)..."
+    if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
+        uv pip install --python "$_VENV_PY" \
+            --upgrade-package unsloth "unsloth>=2026.3.11" unsloth-zoo
+        echo "==> Overlaying local repo (editable)..."
+        uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps
+    else
+        uv pip install --python "$_VENV_PY" \
+            --upgrade-package unsloth "$PACKAGE_NAME"
+    fi
+else
+    # Fallback: GPU detection failed to produce a URL -- let uv resolve torch
+    echo "==> Installing unsloth (this may take a few minutes)..."
+    if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
+        uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.3.11" --torch-backend=auto
+        echo "==> Overlaying local repo (editable)..."
+        uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps
+    else
+        uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto
+    fi
+fi
 
 # ── Run studio setup ──
-# Ensure the venv's Python is on PATH for setup.sh's Python discovery.
-# On macOS the system Python may be outside the 3.11-3.13 range that
-# setup.sh requires, but uv already installed a compatible interpreter
-# inside the venv.
-VENV_ABS_BIN="$(cd "$VENV_NAME/bin" && pwd)"
+# When --local, use the repo's own setup.sh directly.
+# Otherwise, find it inside the installed package.
+SETUP_SH=""
+if [ "$STUDIO_LOCAL_INSTALL" = true ] && [ -f "$_REPO_ROOT/studio/setup.sh" ]; then
+    SETUP_SH="$_REPO_ROOT/studio/setup.sh"
+fi
+
+if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then
+    SETUP_SH=$("$VENV_DIR/bin/python" -c "
+import importlib.resources
+print(importlib.resources.files('studio') / 'setup.sh')
+" 2>/dev/null || echo "")
+fi
+
+# Fallback: search site-packages
+if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then
+    SETUP_SH=$(find "$VENV_DIR" -path "*/studio/setup.sh" -print -quit 2>/dev/null || echo "")
+fi
+
+if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then
+    echo "❌ ERROR: Could not find studio/setup.sh in the installed package."
+    exit 1
+fi
+
+# Ensure the venv's Python is on PATH so setup.sh can find it.
+VENV_ABS_BIN="$(cd "$VENV_DIR/bin" && pwd)"
 if [ -n "$VENV_ABS_BIN" ]; then
     export PATH="$VENV_ABS_BIN:$PATH"
 fi
 
-echo "==> Running unsloth studio setup..."
-REQUESTED_PYTHON_VERSION="$(cd "$VENV_NAME/bin" && pwd)/python" \
-"$VENV_NAME/bin/unsloth" studio setup  Running unsloth setup..."
+if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
+    SKIP_STUDIO_BASE=1 \
+    STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \
+    STUDIO_LOCAL_INSTALL=1 \
+    STUDIO_LOCAL_REPO="$_REPO_ROOT" \
+    bash "$SETUP_SH" /dev/null; then
+                echo '' >> "$_SHELL_PROFILE"
+                echo '# Added by Unsloth installer' >> "$_SHELL_PROFILE"
+                echo 'export PATH="$HOME/.local/bin:$PATH"' >> "$_SHELL_PROFILE"
+                echo "==> Added ~/.local/bin to PATH in $_SHELL_PROFILE"
+            fi
+        fi
+        export PATH="$_LOCAL_BIN:$PATH"
+        ;;
+esac
 
 create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"
 
@@ -694,8 +881,32 @@ echo "   Unsloth Studio installed!"
 echo "========================================="
 echo ""
 
-echo "  To launch, run:"
-echo ""
-echo "    source ${VENV_NAME}/bin/activate"
-echo "    unsloth studio -H 0.0.0.0 -p 8888"
-echo ""
+# Launch studio automatically in interactive terminals;
+# in non-interactive environments (Docker, CI, cloud-init) just print instructions.
+if [ -t 1 ]; then
+    echo "==> Launching Unsloth Studio..."
+    echo ""
+    "$VENV_DIR/bin/unsloth" studio -H 0.0.0.0 -p 8888
+    _LAUNCH_EXIT=$?
+    if [ "$_LAUNCH_EXIT" -ne 0 ] && [ "$_MIGRATED" = true ]; then
+        echo ""
+        echo "⚠️  Unsloth Studio failed to start after migration."
+        echo "   Your migrated environment may be incompatible."
+        echo "   To fix, remove the environment and reinstall:"
+        echo ""
+        echo "   rm -rf $VENV_DIR"
+        echo "   curl -fsSL https://unsloth.ai/install.sh | sh"
+        echo ""
+    fi
+    exit "$_LAUNCH_EXIT"
+else
+    echo "  To launch, run:"
+    echo ""
+    echo "    unsloth studio -H 0.0.0.0 -p 8888"
+    echo ""
+    echo "  Or activate the environment first:"
+    echo ""
+    echo "    source ${VENV_DIR}/bin/activate"
+    echo "    unsloth studio -H 0.0.0.0 -p 8888"
+    echo ""
+fi
diff --git a/studio/backend/colab.py b/studio/backend/colab.py
index 25a9408ccb..ecf9fc2907 100644
--- a/studio/backend/colab.py
+++ b/studio/backend/colab.py
@@ -26,7 +26,7 @@ def _bootstrap_studio_venv() -> None:
     site-packages so that packages like structlog, fastapi, etc. are
     importable from notebook cells and take priority over system copies.
     """
-    venv_lib = Path.home() / ".unsloth" / "studio" / ".venv" / "lib"
+    venv_lib = Path.home() / ".unsloth" / "studio" / "unsloth_studio" / "lib"
     if not venv_lib.exists():
         import warnings
 
diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py
index 9b2678f478..39fec2e6f5 100644
--- a/studio/install_python_stack.py
+++ b/studio/install_python_stack.py
@@ -225,9 +225,20 @@ def _translate_pip_args_for_uv(args: tuple[str, ...]) -> list[str]:
 
 
 def _build_pip_cmd(args: tuple[str, ...]) -> list[str]:
-    """Build a standard pip install command."""
+    """Build a standard pip install command.
+
+    Strips uv-only flags like --upgrade-package that pip doesn't understand.
+    """
     cmd = [sys.executable, "-m", "pip", "install"]
-    cmd.extend(args)
+    skip_next = False
+    for arg in args:
+        if skip_next:
+            skip_next = False
+            continue
+        if arg == "--upgrade-package":
+            skip_next = True  # skip the flag and its value
+            continue
+        cmd.append(arg)
     return cmd
 
 
@@ -241,7 +252,12 @@ def _build_uv_cmd(args: tuple[str, ...]) -> list[str]:
     # the system Python (observed on Colab and similar environments).
     cmd.extend(["--python", sys.executable])
     cmd.extend(_translate_pip_args_for_uv(args))
-    cmd.append("--torch-backend=auto")
+    # Torch is pre-installed by install.sh/setup.ps1.  Do not add
+    # --torch-backend by default -- it can cause solver dead-ends on
+    # CPU-only machines.  Callers that need it can set UV_TORCH_BACKEND.
+    _tb = os.environ.get("UV_TORCH_BACKEND", "")
+    if _tb:
+        cmd.append(f"--torch-backend={_tb}")
     return cmd
 
 
@@ -325,22 +341,90 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None:
 def install_python_stack() -> int:
     global USE_UV, _STEP, _TOTAL
     _STEP = 0
-    _TOTAL = 10 if IS_WINDOWS else 11
 
-    # 1. Upgrade pip (needed even with uv as fallback and for bootstrapping)
-    _progress("pip upgrade")
-    run("Upgrading pip", [sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
+    # When called from install.sh (which already installed unsloth into the venv),
+    # SKIP_STUDIO_BASE=1 is set to avoid redundant reinstallation of base packages.
+    # When called from "unsloth studio update", it is NOT set so base packages
+    # (unsloth + unsloth-zoo) are always reinstalled to pick up new versions.
+    skip_base = os.environ.get("SKIP_STUDIO_BASE", "0") == "1"
+    # When --package is used, install a different package name (e.g. roland-sloth for testing)
+    package_name = os.environ.get("STUDIO_PACKAGE_NAME", "unsloth")
+    # When --local is used, overlay a local repo checkout after updating deps
+    local_repo = os.environ.get("STUDIO_LOCAL_REPO", "")
+    base_total = 10 if IS_WINDOWS else 11
+    _TOTAL = (base_total - 1) if skip_base else base_total
 
-    # Try to use uv for faster installs
+    # 1. Try to use uv for faster installs (must happen before pip upgrade
+    #    because uv venvs don't include pip by default)
     USE_UV = _bootstrap_uv()
 
-    # 2. Core packages: unsloth-zoo + unsloth
-    _progress("base packages")
-    pip_install(
-        "Installing base packages",
-        "--no-cache-dir",
-        req = REQ_ROOT / "base.txt",
-    )
+    # 2. Ensure pip is available (uv venvs created by install.sh don't include pip)
+    _progress("pip bootstrap")
+    if USE_UV:
+        run(
+            "Bootstrapping pip via uv",
+            [
+                "uv",
+                "pip",
+                "install",
+                "--python",
+                sys.executable,
+                "pip",
+            ],
+        )
+    else:
+        run(
+            "Upgrading pip",
+            [sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
+        )
+
+    # 3. Core packages: unsloth-zoo + unsloth (or custom package name)
+    if skip_base:
+        print(_green(f"✅ {package_name} already installed — skipping base packages"))
+    elif local_repo:
+        # Local dev install: update deps from base.txt, then overlay the
+        # local checkout as an editable install (--no-deps so torch is
+        # never re-resolved).
+        _progress("base packages")
+        pip_install(
+            "Updating base packages",
+            "--no-cache-dir",
+            "--upgrade-package",
+            "unsloth",
+            "--upgrade-package",
+            "unsloth-zoo",
+            req = REQ_ROOT / "base.txt",
+        )
+        pip_install(
+            "Overlaying local repo (editable)",
+            "--no-cache-dir",
+            "--no-deps",
+            "-e",
+            local_repo,
+            constrain = False,
+        )
+    elif package_name != "unsloth":
+        # Custom package name (e.g. roland-sloth for testing) — install directly
+        _progress("base packages")
+        pip_install(
+            f"Installing {package_name}",
+            "--no-cache-dir",
+            package_name,
+        )
+    else:
+        # Update path: upgrade only unsloth + unsloth-zoo while preserving
+        # existing torch/CUDA installations.  Torch is pre-installed by
+        # install.sh / setup.ps1; --upgrade-package targets only base pkgs.
+        _progress("base packages")
+        pip_install(
+            "Updating base packages",
+            "--no-cache-dir",
+            "--upgrade-package",
+            "unsloth",
+            "--upgrade-package",
+            "unsloth-zoo",
+            req = REQ_ROOT / "base.txt",
+        )
 
     # 3. Extra dependencies
     _progress("unsloth extras")
diff --git a/studio/setup.ps1 b/studio/setup.ps1
index d429958bf1..c58bcd5c8d 100644
--- a/studio/setup.ps1
+++ b/studio/setup.ps1
@@ -250,9 +250,15 @@ function Find-VsBuildTools {
 # ─────────────────────────────────────────────
 # Banner
 # ─────────────────────────────────────────────
-Write-Host "+==============================================+" -ForegroundColor Green
-Write-Host "|       Unsloth Studio Setup (Windows)         |" -ForegroundColor Green
-Write-Host "+==============================================+" -ForegroundColor Green
+if ($env:SKIP_STUDIO_BASE -eq "1") {
+    Write-Host "+==============================================+" -ForegroundColor Green
+    Write-Host "|       Unsloth Studio Setup (Windows)         |" -ForegroundColor Green
+    Write-Host "+==============================================+" -ForegroundColor Green
+} else {
+    Write-Host "+==============================================+" -ForegroundColor Green
+    Write-Host "|      Unsloth Studio Update (Windows)         |" -ForegroundColor Green
+    Write-Host "+==============================================+" -ForegroundColor Green
+}
 
 # ==========================================================================
 #  PHASE 1: System-level prerequisites (winget installs, env vars)
@@ -1075,9 +1081,9 @@ if (-not $PythonCmd) {
 
 Write-Host "[OK] Using $PythonCmd ($(& $PythonCmd --version 2>&1))" -ForegroundColor Green
 
-# Always create a .venv for isolation -- even for pip installs.
-# Created in the repo root (parent of studio/).
-$VenvDir = Join-Path $env:USERPROFILE ".unsloth\studio\.venv"
+# The venv must already exist (created by install.ps1).
+# This script (setup.ps1 / "unsloth studio update") only updates packages.
+$VenvDir = Join-Path $env:USERPROFILE ".unsloth\studio\unsloth_studio"
 
 # Stale-venv detection: if the venv exists but its torch flavor no longer
 # matches the current machine, wipe it so we get a clean install.
@@ -1140,8 +1146,10 @@ if (Test-Path $VenvDir -PathType Container) {
 }
 
 if (-not (Test-Path $VenvDir)) {
-    Write-Host "   Creating virtual environment at $VenvDir..." -ForegroundColor Cyan
-    & $PythonCmd -m venv $VenvDir
+    Write-Host "[ERROR] Virtual environment not found at $VenvDir" -ForegroundColor Red
+    Write-Host "        Run install.ps1 first to create the environment:" -ForegroundColor Yellow
+    Write-Host "        irm https://unsloth.ai/install.ps1 | iex" -ForegroundColor Yellow
+    exit 1
 } else {
     Write-Host "   Reusing existing virtual environment at $VenvDir" -ForegroundColor Green
 }
@@ -1582,8 +1590,9 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) {
 # Done
 # ============================================
 Write-Host ""
+$doneLine = if ($env:SKIP_STUDIO_BASE -eq "1") { "Setup Complete!" } else { "Update Complete!" }
 Write-Host "+===============================================+" -ForegroundColor Green
-Write-Host "|           Setup Complete!                     |" -ForegroundColor Green
+Write-Host "|           $doneLine                    |" -ForegroundColor Green
 Write-Host "|                                               |" -ForegroundColor Green
 Write-Host "|  Launch with:                                 |" -ForegroundColor Green
 Write-Host "|    unsloth studio -H 0.0.0.0 -p 8888          |" -ForegroundColor Green
diff --git a/studio/setup.sh b/studio/setup.sh
index 97bfbbfadc..0e99173755 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -44,9 +44,15 @@ run_quiet_no_exit() {
     _run_quiet return "$@"
 }
 
-echo "╔══════════════════════════════════════╗"
-echo "║     Unsloth Studio Setup Script      ║"
-echo "╚══════════════════════════════════════╝"
+if [ "${SKIP_STUDIO_BASE:-0}" = "1" ]; then
+    echo "╔══════════════════════════════════════╗"
+    echo "║     Unsloth Studio Setup Script      ║"
+    echo "╚══════════════════════════════════════╝"
+else
+    echo "╔══════════════════════════════════════╗"
+    echo "║     Unsloth Studio Update Script     ║"
+    echo "╚══════════════════════════════════════╝"
+fi
 
 # ── Clean up stale Unsloth compiled caches ──
 rm -rf "$REPO_ROOT/unsloth_compiled_cache"
@@ -244,114 +250,31 @@ fi
 
 # ── 6. Python venv + deps ──
 
-# ── 6a. Discover best Python >= 3.11 and < 3.14 (i.e. 3.11.x, 3.12.x, or 3.13.x) ──
-MIN_PY_MINOR=11   # minimum minor version (>= 3.11)
-MAX_PY_MINOR=13   # maximum minor version (< 3.14)
-BEST_PY=""
-BEST_MINOR=0
-
-# If the caller (e.g. install.sh) already chose a Python, use it directly.
-if [ -n "${REQUESTED_PYTHON_VERSION:-}" ] && [ -x "$REQUESTED_PYTHON_VERSION" ]; then
-    _req_ver=$("$REQUESTED_PYTHON_VERSION" --version 2>&1 | awk '{print $2}')
-    _req_major=$(echo "$_req_ver" | cut -d. -f1)
-    _req_minor=$(echo "$_req_ver" | cut -d. -f2)
-    if [ "$_req_major" -eq 3 ] 2>/dev/null && \
-       [ "$_req_minor" -ge "$MIN_PY_MINOR" ] 2>/dev/null && \
-       [ "$_req_minor" -le "$MAX_PY_MINOR" ] 2>/dev/null; then
-        BEST_PY="$REQUESTED_PYTHON_VERSION"
-        echo "Using requested Python version: $BEST_PY"
-    else
-        echo "Ignoring requested Python $REQUESTED_PYTHON_VERSION ($_req_ver) -- outside supported range"
-    fi
-fi
-
-if [ -z "$BEST_PY" ]; then
-# Collect candidate python3 binaries (python3, python3.9, python3.10, …)
-for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do
-    if ! command -v "$candidate" &>/dev/null; then
-        continue
-    fi
-    # Get version string, e.g. "Python 3.12.5"
-    ver_str=$("$candidate" --version 2>&1) || continue
-    ver_str=$(echo "$ver_str" | awk '{print $2}')
-    py_major=$(echo "$ver_str" | cut -d. -f1)
-    py_minor=$(echo "$ver_str" | cut -d. -f2)
-
-    # Skip anything that isn't Python 3
-    if [ "$py_major" -ne 3 ] 2>/dev/null; then
-        continue
-    fi
-
-    # Skip versions below 3.11
-    if [ "$py_minor" -lt "$MIN_PY_MINOR" ] 2>/dev/null; then
-        continue
-    fi
-
-    # Skip versions above 3.13 (require < 3.14)
-    if [ "$py_minor" -gt "$MAX_PY_MINOR" ] 2>/dev/null; then
-        continue
-    fi
-
-    # Keep the highest qualifying version
-    if [ "$py_minor" -gt "$BEST_MINOR" ]; then
-        BEST_PY="$candidate"
-        BEST_MINOR="$py_minor"
-    fi
-done
-fi
-
-if [ -z "$BEST_PY" ]; then
-    echo "❌ ERROR: No Python version between 3.${MIN_PY_MINOR} and 3.${MAX_PY_MINOR} found on this system."
-    echo "   Detected Python 3 installations:"
-    for candidate in $(compgen -c python3 2>/dev/null | grep -E '^python3(\.[0-9]+)?$' | sort -u); do
-        if command -v "$candidate" &>/dev/null; then
-            echo "     - $candidate ($($candidate --version 2>&1))"
-        fi
-    done
-    echo ""
-    echo "   Please install Python 3.${MIN_PY_MINOR} or 3.${MAX_PY_MINOR}."
-    echo "   For example:  sudo apt install python3.12 python3.12-venv"
-    exit 1
-fi
-
-BEST_VER=$("$BEST_PY" --version 2>&1 | awk '{print $2}')
-echo "✅ Using $BEST_PY ($BEST_VER) — compatible (3.${MIN_PY_MINOR}.x – 3.${MAX_PY_MINOR}.x)"
-
-REQ_ROOT="$SCRIPT_DIR/backend/requirements"
-SINGLE_ENV_CONSTRAINTS="$REQ_ROOT/single-env/constraints.txt"
-SINGLE_ENV_DATA_DESIGNER="$REQ_ROOT/single-env/data-designer.txt"
-SINGLE_ENV_DATA_DESIGNER_DEPS="$REQ_ROOT/single-env/data-designer-deps.txt"
-SINGLE_ENV_PATCH="$REQ_ROOT/single-env/patch_metadata.py"
-
-install_python_stack() {
-    python "$SCRIPT_DIR/install_python_stack.py"
-}
-
-# Create venv under ~/.unsloth/studio/ (shared location, not in repo).
-# All platforms (including Colab) use the same isolated venv so that
-# studio dependencies are never installed into the system Python.
+# The venv must already exist (created by install.sh).
+# This script (setup.sh / "unsloth studio update") only updates packages.
 STUDIO_HOME="$HOME/.unsloth/studio"
-VENV_DIR="$STUDIO_HOME/.venv"
+VENV_DIR="$STUDIO_HOME/unsloth_studio"
 VENV_T5_DIR="$STUDIO_HOME/.venv_t5"
-mkdir -p "$STUDIO_HOME"
 
 # Clean up legacy in-repo venvs if they exist
 [ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv"
 [ -d "$REPO_ROOT/.venv_overlay" ] && rm -rf "$REPO_ROOT/.venv_overlay"
 [ -d "$REPO_ROOT/.venv_t5" ] && rm -rf "$REPO_ROOT/.venv_t5"
+# Note: do NOT delete $STUDIO_HOME/.venv here — install.sh handles migration
 
-rm -rf "$VENV_DIR"
-rm -rf "$VENV_T5_DIR"
-# Try creating venv with pip; fall back to --without-pip + bootstrap
-# (some environments like Colab have broken ensurepip)
-if ! "$BEST_PY" -m venv "$VENV_DIR" 2>/dev/null; then
-    "$BEST_PY" -m venv --without-pip "$VENV_DIR"
-    source "$VENV_DIR/bin/activate"
-    curl -sS https://bootstrap.pypa.io/get-pip.py | python > /dev/null
-else
-    source "$VENV_DIR/bin/activate"
+if [ ! -x "$VENV_DIR/bin/python" ]; then
+    echo "❌ ERROR: Virtual environment not found at $VENV_DIR"
+    echo "   Run install.sh first to create the environment:"
+    echo "   curl -fsSL https://unsloth.ai/install.sh | sh"
+    exit 1
 fi
 
+source "$VENV_DIR/bin/activate"
+
+install_python_stack() {
+    python "$SCRIPT_DIR/install_python_stack.py"
+}
+
 # ── Ensure uv is available (much faster than pip) ──
 USE_UV=false
 if command -v uv &>/dev/null; then
@@ -370,22 +293,53 @@ fast_install() {
 }
 
 cd "$SCRIPT_DIR"
-install_python_stack
 
-# ── 6b. Pre-install transformers 5.x into .venv_t5/ ──
-# Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing
-# at runtime (slow, ~10-15s), we pre-install into a separate directory.
-# The training subprocess just prepends .venv_t5/ to sys.path -- instant switch.
-echo ""
-echo "   Pre-installing transformers 5.x for newer model support..."
-mkdir -p "$VENV_T5_DIR"
-run_quiet "install transformers 5.x" fast_install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0"
-run_quiet "install huggingface_hub for t5" fast_install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.7.1"
-run_quiet "install hf_xet for t5" fast_install --target "$VENV_T5_DIR" --no-deps "hf_xet==1.4.2"
-# tiktoken is needed by Qwen-family tokenizers. Install with deps since
-# regex/requests may be missing on Windows.
-run_quiet "install tiktoken for t5" fast_install --target "$VENV_T5_DIR" "tiktoken"
-echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
+# ── Check if Python deps need updating ──
+# Compare installed package version against PyPI latest.
+# Skip all Python dependency work if versions match (fast update path).
+_PKG_NAME="${STUDIO_PACKAGE_NAME:-unsloth}"
+_SKIP_PYTHON_DEPS=false
+if [ "${SKIP_STUDIO_BASE:-0}" != "1" ] && [ "${STUDIO_LOCAL_INSTALL:-0}" != "1" ]; then
+    # Only check when NOT called from install.sh (which just installed the package)
+    INSTALLED_VER=$("$VENV_DIR/bin/python" -c "
+from importlib.metadata import version
+print(version('$_PKG_NAME'))
+" 2>/dev/null || echo "")
+
+    LATEST_VER=$(curl -fsSL --max-time 5 "https://pypi.org/pypi/$_PKG_NAME/json" 2>/dev/null \
+        | "$VENV_DIR/bin/python" -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null \
+        || echo "")
+
+    if [ -n "$INSTALLED_VER" ] && [ -n "$LATEST_VER" ] && [ "$INSTALLED_VER" = "$LATEST_VER" ]; then
+        echo "✅ $_PKG_NAME $INSTALLED_VER is up to date (matches PyPI latest)"
+        _SKIP_PYTHON_DEPS=true
+    elif [ -n "$INSTALLED_VER" ] && [ -n "$LATEST_VER" ]; then
+        echo "⬆️  $_PKG_NAME $INSTALLED_VER → $LATEST_VER available, updating dependencies..."
+    elif [ -z "$LATEST_VER" ]; then
+        echo "⚠️  Could not reach PyPI, updating dependencies to be safe..."
+    fi
+fi
+
+if [ "$_SKIP_PYTHON_DEPS" = false ]; then
+    install_python_stack
+
+    # ── 6b. Pre-install transformers 5.x into .venv_t5/ ──
+    # Models like GLM-4.7-Flash need transformers>=5.3.0. Instead of pip-installing
+    # at runtime (slow, ~10-15s), we pre-install into a separate directory.
+    # The training subprocess just prepends .venv_t5/ to sys.path -- instant switch.
+    echo ""
+    echo "   Pre-installing transformers 5.x for newer model support..."
+    mkdir -p "$VENV_T5_DIR"
+    run_quiet "install transformers 5.x" fast_install --target "$VENV_T5_DIR" --no-deps "transformers==5.3.0"
+    run_quiet "install huggingface_hub for t5" fast_install --target "$VENV_T5_DIR" --no-deps "huggingface_hub==1.7.1"
+    run_quiet "install hf_xet for t5" fast_install --target "$VENV_T5_DIR" --no-deps "hf_xet==1.4.2"
+    # tiktoken is needed by Qwen-family tokenizers. Install with deps since
+    # regex/requests may be missing on Windows.
+    run_quiet "install tiktoken for t5" fast_install --target "$VENV_T5_DIR" "tiktoken"
+    echo "✅ Transformers 5.x pre-installed to $VENV_T5_DIR/"
+else
+    echo "✅ Python dependencies up to date — skipping"
+fi
 
 # ── 7. WSL: pre-install GGUF build dependencies ──
 # On WSL, sudo requires a password and can't be entered during GGUF export
@@ -651,9 +605,15 @@ rm -rf "$LLAMA_CPP_DIR"
 fi  # end _SKIP_GGUF_BUILD check
 
 echo ""
+if [ "${SKIP_STUDIO_BASE:-0}" = "1" ]; then
+    _DONE_LINE="║          Setup Complete!             ║"
+else
+    _DONE_LINE="║          Update Complete!            ║"
+fi
+
 if [ "$IS_COLAB" = true ]; then
     echo "╔══════════════════════════════════════╗"
-    echo "║           Setup Complete!            ║"
+    echo "$_DONE_LINE"
     echo "╠══════════════════════════════════════╣"
     echo "║ Unsloth Studio is ready to start     ║"
     echo "║ in your Colab notebook!              ║"
@@ -663,7 +623,7 @@ if [ "$IS_COLAB" = true ]; then
     echo "╚══════════════════════════════════════╝"
 else
     echo "╔══════════════════════════════════════╗"
-    echo "║           Setup Complete!            ║"
+    echo "$_DONE_LINE"
     echo "╠══════════════════════════════════════╣"
     echo "║ Launch with:                         ║"
     echo "║                                      ║"
diff --git a/tests/python/__init__.py b/tests/python/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/python/test_cross_platform_parity.py b/tests/python/test_cross_platform_parity.py
new file mode 100644
index 0000000000..6dd41be9fa
--- /dev/null
+++ b/tests/python/test_cross_platform_parity.py
@@ -0,0 +1,137 @@
+"""Cross-platform parity tests between install.sh and install.ps1."""
+
+from __future__ import annotations
+
+import re
+from pathlib import Path
+
+import pytest
+
+REPO_ROOT = Path(__file__).resolve().parents[2]
+INSTALL_SH = REPO_ROOT / "install.sh"
+INSTALL_PS1 = REPO_ROOT / "install.ps1"
+
+
+class TestNoTorchBackendAutoInInstallSh:
+    """install.sh primary install paths must not use --torch-backend=auto.
+
+    The fallback else-branch (when TORCH_INDEX_URL is empty) is allowed to
+    use --torch-backend=auto since that is the last-resort recovery path.
+    """
+
+    def test_no_torch_backend_auto_outside_fallback(self):
+        lines = INSTALL_SH.read_text().splitlines()
+        # Find the fallback block: starts with the "else" after the
+        # TORCH_INDEX_URL check and ends at the next "fi".
+        fallback_start = None
+        fallback_end = None
+        for i, line in enumerate(lines):
+            if fallback_start is None and "GPU detection failed" in line:
+                fallback_start = i
+            elif (
+                fallback_start is not None
+                and fallback_end is None
+                and line.strip() == "fi"
+            ):
+                fallback_end = i
+                break
+        fallback_range = (
+            range(fallback_start or 0, (fallback_end or 0) + 1)
+            if fallback_start
+            else range(0)
+        )
+
+        matches = [
+            (i + 1, line)
+            for i, line in enumerate(lines)
+            if "--torch-backend=auto" in line
+            and not line.lstrip().startswith("#")
+            and i not in fallback_range
+        ]
+        assert matches == [], (
+            f"install.sh contains --torch-backend=auto outside the fallback block at lines: "
+            f"{[m[0] for m in matches]}"
+        )
+
+    def test_fallback_uses_torch_backend_auto(self):
+        """The fallback branch should use --torch-backend=auto as recovery."""
+        text = INSTALL_SH.read_text()
+        assert (
+            "GPU detection failed" in text
+        ), "install.sh should have a fallback branch for when GPU detection fails"
+
+
+class TestInstallShHasGpuDetection:
+    """install.sh must contain the get_torch_index_url function."""
+
+    def test_function_exists(self):
+        text = INSTALL_SH.read_text()
+        assert (
+            "get_torch_index_url()" in text
+        ), "install.sh is missing the get_torch_index_url() function"
+
+    def test_torch_index_url_assigned(self):
+        text = INSTALL_SH.read_text()
+        assert (
+            "TORCH_INDEX_URL=$(get_torch_index_url)" in text
+        ), "install.sh should assign TORCH_INDEX_URL from get_torch_index_url()"
+
+
+class TestCudaMappingParity:
+    """CUDA version thresholds must match between install.sh and install.ps1."""
+
+    @staticmethod
+    def _extract_cuda_thresholds_sh(text: str) -> list[str]:
+        """Extract cu* suffixes from the major/minor comparison chain in install.sh."""
+        # Only match lines in the if/elif chain that compare _major/_minor
+        in_func = False
+        results = []
+        for line in text.splitlines():
+            if "get_torch_index_url()" in line:
+                in_func = True
+                continue
+            if in_func and line.startswith("}"):
+                break
+            if in_func and ("_major" in line or "_minor" in line):
+                m = re.search(r"/(cu\d+|cpu)", line)
+                if m:
+                    results.append(m.group(1))
+        return results
+
+    @staticmethod
+    def _extract_cuda_thresholds_ps1(text: str) -> list[str]:
+        """Extract cu* suffixes from the major/minor comparison chain in install.ps1."""
+        in_func = False
+        depth = 0
+        results = []
+        for line in text.splitlines():
+            if "function Get-TorchIndexUrl" in line:
+                in_func = True
+                depth = 1
+                continue
+            if in_func:
+                depth += line.count("{") - line.count("}")
+                if depth <= 0:
+                    break
+                # Only match the if-chain lines that compare $major/$minor
+                if "$major" in line or "$minor" in line:
+                    m = re.search(r"/(cu\d+|cpu)", line)
+                    if m:
+                        results.append(m.group(1))
+        return results
+
+    def test_same_cuda_suffixes(self):
+        """Both scripts should produce the same ordered list of CUDA index suffixes."""
+        sh_text = INSTALL_SH.read_text()
+        ps1_text = INSTALL_PS1.read_text()
+
+        sh_thresholds = self._extract_cuda_thresholds_sh(sh_text)
+        ps1_thresholds = self._extract_cuda_thresholds_ps1(ps1_text)
+
+        assert len(sh_thresholds) > 0, "Could not extract thresholds from install.sh"
+        assert len(ps1_thresholds) > 0, "Could not extract thresholds from install.ps1"
+        assert sh_thresholds == ps1_thresholds, (
+            f"CUDA mapping mismatch:\n"
+            f"  install.sh:  {sh_thresholds}\n"
+            f"  install.ps1: {ps1_thresholds}"
+        )
diff --git a/tests/python/test_install_python_stack.py b/tests/python/test_install_python_stack.py
new file mode 100644
index 0000000000..16538ae42b
--- /dev/null
+++ b/tests/python/test_install_python_stack.py
@@ -0,0 +1,56 @@
+"""Tests for install_python_stack._build_uv_cmd torch-backend handling."""
+
+from __future__ import annotations
+
+import importlib
+import os
+import sys
+from pathlib import Path
+from unittest import mock
+
+import pytest
+
+# Add the studio directory so we can import install_python_stack
+STUDIO_DIR = Path(__file__).resolve().parents[2] / "studio"
+sys.path.insert(0, str(STUDIO_DIR))
+
+# _build_uv_cmd lives at module level; import after path setup.
+# We need to mock parts of the module that do work at import time.
+import install_python_stack as ips
+
+
+class TestBuildUvCmdTorchBackend:
+    """Verify _build_uv_cmd only adds --torch-backend when UV_TORCH_BACKEND is set."""
+
+    def _call(self, args: tuple[str, ...] = ()) -> list[str]:
+        return ips._build_uv_cmd(args)
+
+    def test_default_no_torch_backend(self):
+        """Without UV_TORCH_BACKEND env var, no --torch-backend flag."""
+        env = os.environ.copy()
+        env.pop("UV_TORCH_BACKEND", None)
+        with mock.patch.dict(os.environ, env, clear = True):
+            cmd = self._call(("somepackage",))
+        assert not any(
+            a.startswith("--torch-backend") for a in cmd
+        ), f"--torch-backend should not appear by default, got: {cmd}"
+
+    def test_uv_torch_backend_auto(self):
+        """UV_TORCH_BACKEND=auto adds --torch-backend=auto."""
+        with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "auto"}):
+            cmd = self._call(("somepackage",))
+        assert "--torch-backend=auto" in cmd
+
+    def test_uv_torch_backend_cpu(self):
+        """UV_TORCH_BACKEND=cpu adds --torch-backend=cpu."""
+        with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": "cpu"}):
+            cmd = self._call(("somepackage",))
+        assert "--torch-backend=cpu" in cmd
+
+    def test_uv_torch_backend_empty(self):
+        """UV_TORCH_BACKEND="" (empty string) should NOT add --torch-backend."""
+        with mock.patch.dict(os.environ, {"UV_TORCH_BACKEND": ""}):
+            cmd = self._call(("somepackage",))
+        assert not any(
+            a.startswith("--torch-backend") for a in cmd
+        ), f"Empty UV_TORCH_BACKEND should not add flag, got: {cmd}"
diff --git a/tests/run_all.sh b/tests/run_all.sh
new file mode 100755
index 0000000000..d7fdb38e74
--- /dev/null
+++ b/tests/run_all.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+# Run all installer tests.
+set -e
+
+TESTS_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo "=== Bash tests ==="
+sh "$TESTS_DIR/sh/test_get_torch_index_url.sh"
+
+echo ""
+echo "=== Python tests ==="
+python -m pytest "$TESTS_DIR/python/test_install_python_stack.py" -v
+python -m pytest "$TESTS_DIR/python/test_cross_platform_parity.py" -v
+
+echo ""
+echo "All tests passed."
diff --git a/tests/sh/test_get_torch_index_url.sh b/tests/sh/test_get_torch_index_url.sh
new file mode 100755
index 0000000000..6387922712
--- /dev/null
+++ b/tests/sh/test_get_torch_index_url.sh
@@ -0,0 +1,128 @@
+#!/bin/bash
+# Unit tests for get_torch_index_url() from install.sh
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+INSTALL_SH="$SCRIPT_DIR/../../install.sh"
+PASS=0
+FAIL=0
+
+# Extract only the get_torch_index_url function from install.sh
+# Also replace the hardcoded /usr/bin/nvidia-smi fallback with a
+# controllable path so we can test the "no GPU" scenario on GPU machines.
+_FUNC_FILE=$(mktemp)
+_FAKE_SMI_DIR=$(mktemp -d)
+sed -n '/^get_torch_index_url()/,/^}/p' "$INSTALL_SH" \
+    | sed "s|/usr/bin/nvidia-smi|$_FAKE_SMI_DIR/nvidia-smi-absent|g" \
+    > "$_FUNC_FILE"
+
+# Save system PATH so we always have basic tools (uname, grep, head, etc.)
+_SYS_PATH="/usr/local/bin:/usr/bin:/bin"
+
+assert_eq() {
+    _label="$1"; _expected="$2"; _actual="$3"
+    if [ "$_actual" = "$_expected" ]; then
+        echo "  PASS: $_label"
+        PASS=$((PASS + 1))
+    else
+        echo "  FAIL: $_label (expected '$_expected', got '$_actual')"
+        FAIL=$((FAIL + 1))
+    fi
+}
+
+# Helper: create a mock nvidia-smi that prints a given CUDA version string
+make_mock_smi() {
+    _dir=$(mktemp -d)
+    cat > "$_dir/nvidia-smi" </dev/null || true)
+    [ -n "$_real" ] && ln -sf "$_real" "$_TOOLS_DIR/$_cmd"
+done
+
+# Helper: run get_torch_index_url with a custom PATH
+# $1 = directory with mock nvidia-smi (prepended to PATH), or "none" for no-GPU test
+run_func() {
+    _mock_dir="$1"
+    if [ "$_mock_dir" = "none" ]; then
+        # Minimal PATH with only basic tools, no nvidia-smi anywhere
+        PATH="$_TOOLS_DIR" bash -c ". '$_FUNC_FILE'; get_torch_index_url" 2>/dev/null
+    else
+        # Put mock nvidia-smi dir first, then basic tools
+        PATH="$_mock_dir:$_TOOLS_DIR" bash -c ". '$_FUNC_FILE'; get_torch_index_url" 2>/dev/null
+    fi
+}
+
+echo "=== test_get_torch_index_url ==="
+
+# 1) No nvidia-smi available -> cpu
+_result=$(run_func "none")
+assert_eq "no nvidia-smi -> cpu" "https://download.pytorch.org/whl/cpu" "$_result"
+
+# 2) CUDA 12.6 -> cu126
+_dir=$(make_mock_smi "12.6")
+_result=$(run_func "$_dir")
+assert_eq "CUDA 12.6 -> cu126" "https://download.pytorch.org/whl/cu126" "$_result"
+rm -rf "$_dir"
+
+# 3) CUDA 12.8 -> cu128
+_dir=$(make_mock_smi "12.8")
+_result=$(run_func "$_dir")
+assert_eq "CUDA 12.8 -> cu128" "https://download.pytorch.org/whl/cu128" "$_result"
+rm -rf "$_dir"
+
+# 4) CUDA 13.0 -> cu130
+_dir=$(make_mock_smi "13.0")
+_result=$(run_func "$_dir")
+assert_eq "CUDA 13.0 -> cu130" "https://download.pytorch.org/whl/cu130" "$_result"
+rm -rf "$_dir"
+
+# 5) CUDA 12.4 -> cu124
+_dir=$(make_mock_smi "12.4")
+_result=$(run_func "$_dir")
+assert_eq "CUDA 12.4 -> cu124" "https://download.pytorch.org/whl/cu124" "$_result"
+rm -rf "$_dir"
+
+# 6) CUDA 11.8 -> cu118
+_dir=$(make_mock_smi "11.8")
+_result=$(run_func "$_dir")
+assert_eq "CUDA 11.8 -> cu118" "https://download.pytorch.org/whl/cu118" "$_result"
+rm -rf "$_dir"
+
+# 7) CUDA 10.2 (too old) -> cpu
+_dir=$(make_mock_smi "10.2")
+_result=$(run_func "$_dir")
+assert_eq "CUDA 10.2 -> cpu" "https://download.pytorch.org/whl/cpu" "$_result"
+rm -rf "$_dir"
+
+# 8) Unparseable nvidia-smi output -> cu126 default
+_dir=$(mktemp -d)
+cat > "$_dir/nvidia-smi" <<'MOCK'
+#!/bin/sh
+echo "something completely unexpected"
+MOCK
+chmod +x "$_dir/nvidia-smi"
+_result=$(run_func "$_dir")
+assert_eq "unparseable -> cu126" "https://download.pytorch.org/whl/cu126" "$_result"
+rm -rf "$_dir"
+
+rm -f "$_FUNC_FILE"
+rm -rf "$_FAKE_SMI_DIR"
+rm -rf "$_TOOLS_DIR"
+
+echo ""
+echo "Results: $PASS passed, $FAIL failed"
+[ "$FAIL" -eq 0 ] || exit 1
diff --git a/unsloth_cli/__init__.py b/unsloth_cli/__init__.py
index 3b9043c5bf..3a821359b7 100644
--- a/unsloth_cli/__init__.py
+++ b/unsloth_cli/__init__.py
@@ -6,7 +6,6 @@ import typer
 from unsloth_cli.commands.train import train
 from unsloth_cli.commands.inference import inference
 from unsloth_cli.commands.export import export, list_checkpoints
-from unsloth_cli.commands.ui import ui
 from unsloth_cli.commands.studio import studio_app
 
 app = typer.Typer(
@@ -18,5 +17,4 @@ app.command()(train)
 app.command()(inference)
 app.command()(export)
 app.command("list-checkpoints")(list_checkpoints)
-app.command()(ui)
 app.add_typer(studio_app, name = "studio", help = "Unsloth Studio commands.")
diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py
index 192e138a9c..c6d398eebd 100644
--- a/unsloth_cli/commands/studio.py
+++ b/unsloth_cli/commands/studio.py
@@ -22,9 +22,9 @@ _PACKAGE_ROOT = Path(__file__).resolve().parent.parent.parent
 def _studio_venv_python() -> Optional[Path]:
     """Return the studio venv Python binary, or None if not set up."""
     if platform.system() == "Windows":
-        p = STUDIO_HOME / ".venv" / "Scripts" / "python.exe"
+        p = STUDIO_HOME / "unsloth_studio" / "Scripts" / "python.exe"
     else:
-        p = STUDIO_HOME / ".venv" / "bin" / "python"
+        p = STUDIO_HOME / "unsloth_studio" / "bin" / "python"
     return p if p.is_file() else None
 
 
@@ -44,7 +44,7 @@ def _find_run_py() -> Optional[Path]:
         "lib/python*/site-packages/studio/backend/run.py",
         "Lib/site-packages/studio/backend/run.py",
     ):
-        for match in (STUDIO_HOME / ".venv").glob(pattern):
+        for match in (STUDIO_HOME / "unsloth_studio").glob(pattern):
             return match
     return None
 
@@ -64,7 +64,7 @@ def _find_setup_script() -> Optional[Path]:
         f"lib/python*/site-packages/studio/{name}",
         f"Lib/site-packages/studio/{name}",
     ):
-        for match in (STUDIO_HOME / ".venv").glob(pattern):
+        for match in (STUDIO_HOME / "unsloth_studio").glob(pattern):
             return match
     return None
 
@@ -85,7 +85,7 @@ def studio_default(
         return
 
     # Always use the studio venv if it exists and we're not already in it
-    studio_venv_dir = STUDIO_HOME / ".venv"
+    studio_venv_dir = STUDIO_HOME / "unsloth_studio"
     in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
 
     if not in_studio_venv:
@@ -132,7 +132,7 @@ def studio_default(
             else:
                 os.execvp(str(studio_python), args)
         else:
-            typer.echo("Studio not set up. Run 'unsloth studio setup' first.")
+            typer.echo("Studio not set up. Run install.sh first.")
             raise typer.Exit(1)
 
     from studio.backend.run import run_server
@@ -166,12 +166,11 @@ def studio_default(
         typer.echo("\nShutting down...")
 
 
-# ── unsloth studio setup ─────────────────────────────────────────────
+# ── unsloth studio setup / update ─────────────────────────────────────
 
 
-@studio_app.command()
-def setup():
-    """Run one-time Studio environment setup."""
+def _run_setup_script() -> None:
+    """Find and run the studio setup/update script."""
     script = _find_setup_script()
     if not script:
         typer.echo("Error: Could not find setup script (setup.sh / setup.ps1).")
@@ -188,6 +187,35 @@ def setup():
         raise typer.Exit(result.returncode)
 
 
+@studio_app.command(hidden = True)
+def setup():
+    """Deprecated: use 'unsloth studio update' or re-run install.sh."""
+    typer.echo(
+        "Note: 'unsloth studio setup' is deprecated. Use 'unsloth studio update' or re-run install.sh."
+    )
+    _run_setup_script()
+
+
+@studio_app.command()
+def update(
+    local: bool = typer.Option(
+        False, "--local", help = "Install from local repo instead of PyPI"
+    ),
+    package: str = typer.Option(
+        "unsloth", "--package", help = "Package name to install/update (for testing)"
+    ),
+):
+    """Update Unsloth Studio dependencies and rebuild."""
+    os.environ["STUDIO_LOCAL_INSTALL"] = "1" if local else "0"
+    os.environ["STUDIO_PACKAGE_NAME"] = package
+    if local:
+        # Pass the repo root explicitly so install_python_stack.py doesn't
+        # have to guess from SCRIPT_DIR (which may be inside site-packages).
+        repo_root = Path(__file__).resolve().parents[2]
+        os.environ["STUDIO_LOCAL_REPO"] = str(repo_root)
+    _run_setup_script()
+
+
 # ── unsloth studio reset-password ────────────────────────────────────
 
 
diff --git a/unsloth_cli/commands/ui.py b/unsloth_cli/commands/ui.py
deleted file mode 100644
index 8f76636990..0000000000
--- a/unsloth_cli/commands/ui.py
+++ /dev/null
@@ -1,103 +0,0 @@
-# SPDX-License-Identifier: AGPL-3.0-only
-# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
-
-import os
-import sys
-import time
-from pathlib import Path
-from typing import Optional
-
-import typer
-
-
-def ui(
-    port: int = typer.Option(
-        8888, "--port", "-p", help = "Port to run the UI server on."
-    ),
-    host: str = typer.Option(
-        "0.0.0.0", "--host", "-H", help = "Host address to bind to."
-    ),
-    frontend: Optional[Path] = typer.Option(
-        None, "--frontend", "-f", help = "Path to frontend build directory."
-    ),
-    silent: bool = typer.Option(
-        False, "--silent", "-q", help = "Suppress startup messages."
-    ),
-):
-    """Launch the Unsloth web UI backend server (alias for 'unsloth studio')."""
-    from unsloth_cli.commands.studio import (
-        _studio_venv_python,
-        _find_run_py,
-        STUDIO_HOME,
-    )
-
-    # Re-execute in studio venv if available and not already inside it
-    studio_venv_dir = STUDIO_HOME / ".venv"
-    in_studio_venv = sys.prefix.startswith(str(studio_venv_dir))
-
-    if not in_studio_venv:
-        studio_python = _studio_venv_python()
-        run_py = _find_run_py()
-        if studio_python and run_py:
-            if not silent:
-                typer.echo("Launching Unsloth Studio... Please wait...")
-            args = [
-                str(studio_python),
-                str(run_py),
-                "--host",
-                host,
-                "--port",
-                str(port),
-            ]
-            if frontend:
-                args.extend(["--frontend", str(frontend)])
-            if silent:
-                args.append("--silent")
-            # On Windows, os.execvp() spawns a child but the parent lingers,
-            # so Ctrl+C only kills the parent leaving the child orphaned.
-            # Use subprocess.run() on Windows so the parent waits for the child.
-            if sys.platform == "win32":
-                import subprocess as _sp
-
-                proc = _sp.Popen(args)
-                try:
-                    rc = proc.wait()
-                except KeyboardInterrupt:
-                    # Child has its own signal handler — let it finish
-                    rc = proc.wait()
-                raise typer.Exit(rc)
-            else:
-                os.execvp(str(studio_python), args)
-        else:
-            typer.echo("Studio not set up. Run 'unsloth studio setup' first.")
-            raise typer.Exit(1)
-
-    from studio.backend.run import run_server
-
-    if not silent:
-        from studio.backend.run import _resolve_external_ip
-
-        display_host = _resolve_external_ip() if host == "0.0.0.0" else host
-        typer.echo(f"Starting Unsloth Studio on http://{display_host}:{port}")
-
-    run_kwargs = dict(host = host, port = port, silent = silent)
-    if frontend is not None:
-        run_kwargs["frontend_path"] = frontend
-    run_server(**run_kwargs)
-
-    from studio.backend.run import _shutdown_event
-
-    try:
-        if _shutdown_event is not None:
-            # NOTE: Event.wait() without a timeout blocks at the C level
-            # on Linux, preventing Python from delivering SIGINT (Ctrl+C).
-            while not _shutdown_event.is_set():
-                _shutdown_event.wait(timeout = 1)
-        else:
-            while True:
-                time.sleep(1)
-    except KeyboardInterrupt:
-        from studio.backend.run import _graceful_shutdown, _server
-
-        _graceful_shutdown(_server)
-        typer.echo("\nShutting down...")

From cc1be75621c17d023d04c6334dff143ee2ad5e84 Mon Sep 17 00:00:00 2001
From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
Date: Wed, 25 Mar 2026 12:32:31 +0000
Subject: [PATCH 23/34] studio: stabilize reasoning panel scroll behavior and
 prevent composer overlap (#4587)

* fix(studio): reasoning panel scroll and thread footer overlap

* refactor(studio): dedupe reasoning scroll lock teardown
---
 .../src/components/assistant-ui/reasoning.tsx | 73 +++++++++++++++++--
 .../src/components/assistant-ui/thread.tsx    |  2 +-
 .../src/components/ui/collapsible.tsx         | 15 ++--
 3 files changed, 79 insertions(+), 11 deletions(-)

diff --git a/studio/frontend/src/components/assistant-ui/reasoning.tsx b/studio/frontend/src/components/assistant-ui/reasoning.tsx
index 0e37f6d433..6b2c7a05e7 100644
--- a/studio/frontend/src/components/assistant-ui/reasoning.tsx
+++ b/studio/frontend/src/components/assistant-ui/reasoning.tsx
@@ -17,7 +17,6 @@ import {
   type ReasoningGroupComponent,
   type ReasoningMessagePartComponent,
   useAuiState,
-  useScrollLock,
 } from "@assistant-ui/react";
 import { copyToClipboard } from "@/lib/copy-to-clipboard";
 import { Idea01Icon } from "@hugeicons/core-free-icons";
@@ -34,6 +33,7 @@ import {
   useState,
 } from "react";
 const ANIMATION_DURATION = 200;
+const AUTO_SCROLL_THRESHOLD_PX = 24;
 
 export const reasoningVariants = cva("aui-reasoning-root mb-4 w-full", {
   variants: {
@@ -68,8 +68,49 @@ function ReasoningRoot({
   ...props
 }: ReasoningRootProps) {
   const collapsibleRef = useRef(null);
+  const lockCleanupRef = useRef<(() => void) | null>(null);
   const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
-  const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);
+
+  useEffect(() => {
+    return () => {
+      lockCleanupRef.current?.();
+    };
+  }, []);
+
+  const lockScroll = useCallback(() => {
+    lockCleanupRef.current?.();
+
+    const animatedElement = collapsibleRef.current;
+    if (!animatedElement) return;
+
+    let scrollContainer: HTMLElement | null = animatedElement;
+    while (scrollContainer) {
+      const { overflowY } = getComputedStyle(scrollContainer);
+      if (overflowY === "scroll" || overflowY === "auto") {
+        break;
+      }
+      scrollContainer = scrollContainer.parentElement;
+    }
+    if (!scrollContainer) return;
+
+    const scrollPosition = scrollContainer.scrollTop;
+    const resetPosition = () => {
+      scrollContainer.scrollTop = scrollPosition;
+    };
+
+    scrollContainer.addEventListener("scroll", resetPosition);
+    let timeoutId: ReturnType | null = null;
+    const cleanup = () => {
+      if (timeoutId !== null) {
+        clearTimeout(timeoutId);
+        timeoutId = null;
+      }
+      scrollContainer.removeEventListener("scroll", resetPosition);
+      lockCleanupRef.current = null;
+    };
+    timeoutId = setTimeout(cleanup, ANIMATION_DURATION);
+    lockCleanupRef.current = cleanup;
+  }, []);
 
   const isControlled = controlledOpen !== undefined;
   const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
@@ -220,6 +261,8 @@ function ReasoningText({
 }: ComponentProps<"div"> & { streaming?: boolean }) {
   const scrollRef = useRef(null);
   const shouldAutoScrollRef = useRef(true);
+  const detachedFromBottomRef = useRef(false);
+  const lastScrollTopRef = useRef(0);
 
   useEffect(() => {
     if (!(streaming && scrollRef.current)) {
@@ -227,8 +270,25 @@ function ReasoningText({
     }
     const el = scrollRef.current;
     const updateAutoScroll = () => {
+      const currentScrollTop = el.scrollTop;
+      if (currentScrollTop < lastScrollTopRef.current) {
+        detachedFromBottomRef.current = true;
+      }
       const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
-      shouldAutoScrollRef.current = distanceFromBottom <= 24;
+      if (
+        detachedFromBottomRef.current &&
+        distanceFromBottom <= AUTO_SCROLL_THRESHOLD_PX
+      ) {
+        detachedFromBottomRef.current = false;
+      }
+      shouldAutoScrollRef.current = !detachedFromBottomRef.current;
+      lastScrollTopRef.current = currentScrollTop;
+    };
+    const handleWheel = (event: WheelEvent) => {
+      if (event.deltaY < 0) {
+        detachedFromBottomRef.current = true;
+        shouldAutoScrollRef.current = false;
+      }
     };
     const observer = new MutationObserver(() => {
       if (shouldAutoScrollRef.current) {
@@ -236,16 +296,19 @@ function ReasoningText({
       }
     });
     el.addEventListener("scroll", updateAutoScroll);
+    el.addEventListener("wheel", handleWheel, { passive: true });
     observer.observe(el, {
       childList: true,
       subtree: true,
       characterData: true,
     });
-    shouldAutoScrollRef.current = true;
-    el.scrollTop = el.scrollHeight;
+    lastScrollTopRef.current = el.scrollTop;
+    detachedFromBottomRef.current = false;
+    updateAutoScroll();
     return () => {
       observer.disconnect();
       el.removeEventListener("scroll", updateAutoScroll);
+      el.removeEventListener("wheel", handleWheel);
     };
   }, [streaming]);
 
diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx
index 0c07e133fb..d688822815 100644
--- a/studio/frontend/src/components/assistant-ui/thread.tsx
+++ b/studio/frontend/src/components/assistant-ui/thread.tsx
@@ -89,7 +89,7 @@ export const Thread: FC<{ hideComposer?: boolean; hideWelcome?: boolean }> = ({
           }}
         />
 
-        
+        
           
            !thread.isEmpty}>
             {!hideComposer && }
diff --git a/studio/frontend/src/components/ui/collapsible.tsx b/studio/frontend/src/components/ui/collapsible.tsx
index 3566eb9859..df5347c1a7 100644
--- a/studio/frontend/src/components/ui/collapsible.tsx
+++ b/studio/frontend/src/components/ui/collapsible.tsx
@@ -2,13 +2,18 @@
 // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
 
 import { cn } from "@/lib/utils";
+import * as React from "react";
 import { Collapsible as CollapsiblePrimitive } from "radix-ui";
 
-function Collapsible({
-  ...props
-}: React.ComponentProps) {
-  return ;
-}
+const Collapsible = React.forwardRef<
+  React.ElementRef,
+  React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+  return (
+    
+  );
+});
+Collapsible.displayName = CollapsiblePrimitive.Root.displayName;
 
 function CollapsibleTrigger({
   ...props

From f4d8a246bf4454f23e73dca095ae30802ccc9c8a Mon Sep 17 00:00:00 2001
From: DoubleMathew 
Date: Wed, 25 Mar 2026 07:42:43 -0500
Subject: [PATCH 24/34] Use prebuilt llama.cpp for unsloth studio setup (#4562)

* Use prebuilt llama.cpp for unsloth studio setup

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

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

* Fix 3 issues that cause unnecessary fallback to source build

1. Make filelock import optional -- environments without filelock
   (e.g. minimal installs) crashed at import time instead of
   gracefully skipping the lock.

2. Use already-verified converter script from the hydrated source
   tree instead of re-downloading from raw.githubusercontent.com
   with no checksum. Adds symlink with copy fallback for the
   legacy filename.

3. Initialize $SkipPrebuiltInstall in setup.ps1 before first use
   to prevent potential uninitialized variable errors.

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

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

* Keep network fallback in ensure_converter_scripts

Prefer the local verified copy from the hydrated source tree, but
retain the original network download as a fallback if the file is
missing. Create the legacy hyphenated filename as a symlink with a
copy fallback instead of writing a second full copy.

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

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

* Fix 4 bugs in source-build fallback and binary_env paths

- setup.ps1: Replace git pull + checkout FETCH_HEAD with fetch + checkout -B
  to avoid detached HEAD state that breaks re-runs. Use pinned tag in both
  fetch and clone paths.
- setup.sh: Move rm -rf after cmake/git prerequisite checks so a missing
  tool no longer deletes the existing install. Add --branch tag to clone.
- install_llama_prebuilt.py: Add binary_path.parent to Linux LD_LIBRARY_PATH
  in binary_env() so bundled .so files in build/bin are found even without
  RPATH, matching the existing Windows PATH logic.
- Add test for binary_env LD_LIBRARY_PATH on Linux.

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

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

* Handle unresolved "latest" tag in source-build fallback clone

When tag resolution fails and the requested tag is "latest", both
setup scripts now omit --branch from git clone so the default branch
is cloned instead of failing on a nonexistent "latest" branch/tag.
Similarly, the PS1 fetch path fetches the default ref when the tag
is "latest".

* Resolve actual latest ggml-org tag instead of using literal "latest"

When both Python tag resolution attempts fail and the requested tag
is "latest", query the GitHub API for the actual latest release tag
from ggml-org/llama.cpp (e.g. b8508) instead of passing the literal
string "latest" to git clone --branch, which would fail since no
such branch/tag exists.

setup.sh uses curl + python json parsing; setup.ps1 uses
Invoke-RestMethod. Both fall back to the raw requested tag if the
API call also fails.

* Try Unsloth release repo before ggml-org when resolving latest tag

When falling back to the GitHub API to resolve "latest", query the
Unsloth release repo (unslothai/llama.cpp) first since it has the
prebuilt binaries pinned to tested tags. Only fall back to
ggml-org/llama.cpp if the Unsloth repo query fails.

* Add comprehensive sandbox tests for PR #4562 bug fixes

35 tests covering all fixes across platforms:
- binary_env cross-platform (Linux LD_LIBRARY_PATH, Windows PATH,
  macOS DYLD_LIBRARY_PATH) with edge cases (dedup, ordering, existing paths)
- resolve_requested_llama_tag (concrete, latest, None, empty)
- setup.sh logic via subprocess: prereq check ordering (cmake/git missing
  preserves install), pinned tag in clone, fetch+checkout -B pattern,
  fetch failure warns instead of aborting
- "latest" tag resolution fallback chain (Unsloth API -> ggml-org ->
  raw) with mock curl: success, failure, malformed JSON, empty body,
  empty tag_name, env overrides
- Source code pattern verification for both .sh and .ps1 files

All 138 tests pass in isolated uv venv.

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

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

* Add binary_path.parent to macOS DYLD_LIBRARY_PATH in binary_env

macOS prebuilt .dylib files are overlaid into build/bin (same as
Linux), but binary_env only added install_dir to DYLD_LIBRARY_PATH.
Add binary_path.parent so the loader can find sibling dylibs even
without embedded loader paths.

Mirrors the existing fix for Linux LD_LIBRARY_PATH and the Windows
PATH pattern.

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

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

* Guard --branch when resolved tag is "latest"; fix broken test assertion

When all API fallbacks fail and the tag stays as literal "latest",
omit --branch from git clone (clones default branch instead of
failing). Both setup.sh and setup.ps1 now check for "latest" before
passing --branch to git clone/fetch.

Also fix test_setup_ps1_clone_uses_branch_tag which used Python
tuple syntax (assert "x", "y" in z) that always passes. Changed to
assert "x" in z and "y" in z.

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

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

* Fix macOS DYLD trailing colon, install_lock no-op, and debug log

- binary_env macOS: use dedupe_existing_dirs instead of raw string
  concatenation. Eliminates trailing colon in DYLD_LIBRARY_PATH
  (which causes dyld to search CWD for libraries) and deduplicates
  when binary_path.parent == install_dir. Now consistent with the
  Linux and Windows branches.
- install_lock: when filelock is not installed, use os.O_CREAT|O_EXCL
  as a fallback exclusive file lock with timeout, instead of yielding
  with no locking. Prevents concurrent installs from corrupting each
  other's staging directories.
- setup.ps1: remove [DEBUG] log line that printed to every user on
  every Windows setup run.

* Add stale-lock detection and atomic clone-then-swap

install_lock fallback (no filelock): write PID to lock file and
check if the holder process is still alive on contention. Dead PIDs
(ProcessLookupError) and unreadable lock files trigger immediate
cleanup. Live processes owned by other users (PermissionError) are
correctly recognized as alive -- the lock is not removed.

setup.sh/setup.ps1 source-build: clone into a temporary directory
first, then swap into place only on success. If git clone fails,
the existing install is preserved instead of being deleted by the
premature rm -rf.

* Remove redundant upstream_tag != release_tag check

load_approved_release_checksums compared checksums.upstream_tag
against the Unsloth release_tag, which are different namespaces
(upstream ggml-org tag vs Unsloth published tag). This only worked
because both happened to be "b8508" by convention. Would break if
Unsloth ever uses a different release naming scheme.

The existing check at parse_approved_release_checksums (line 950)
already validates the release_tag field correctly.

* Fix lock TOCTOU race and build-in-temp-dir swap

install_lock fallback: add os.fsync(fd) after writing PID to ensure
the PID is visible to racing processes before they check. Treat
empty lock files (PID not yet written) as "wait and retry" instead
of stale, closing the window where two processes could both see an
empty file, both unlink it, and both acquire the lock.

setup.sh/setup.ps1 source-build: clone AND build in a temp directory
(LLAMA_CPP_DIR.build.$$). Only swap into the final LLAMA_CPP_DIR
after the build succeeds. If clone or cmake or build fails, the temp
dir is cleaned up and the existing working install is preserved.
Previously, rm -rf ran after clone but before build, destroying the
existing install even if the build later failed.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han 
---
 studio/install_llama_prebuilt.py              | 3395 +++++++++++++++++
 studio/setup.ps1                              |  226 +-
 studio/setup.sh                               |  127 +-
 .../install/smoke_test_llama_prebuilt.py      |  142 +
 .../test_install_llama_prebuilt_logic.py      |  630 +++
 tests/studio/install/test_pr4562_bugfixes.py  |  687 ++++
 tests/studio/install/test_selection_logic.py  |  903 +++++
 7 files changed, 6046 insertions(+), 64 deletions(-)
 create mode 100755 studio/install_llama_prebuilt.py
 create mode 100644 tests/studio/install/smoke_test_llama_prebuilt.py
 create mode 100644 tests/studio/install/test_install_llama_prebuilt_logic.py
 create mode 100644 tests/studio/install/test_pr4562_bugfixes.py
 create mode 100644 tests/studio/install/test_selection_logic.py

diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py
new file mode 100755
index 0000000000..a9d0b72352
--- /dev/null
+++ b/studio/install_llama_prebuilt.py
@@ -0,0 +1,3395 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: AGPL-3.0-only
+# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+"""Cross platform llama.cpp prebuilt installer for Unsloth Studio"""
+
+from __future__ import annotations
+
+import argparse
+import fnmatch
+import hashlib
+import json
+import os
+import platform
+import random
+import shutil
+import site
+import socket
+import subprocess
+import sys
+import tarfile
+import tempfile
+import textwrap
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+import zipfile
+from contextlib import contextmanager
+from dataclasses import dataclass
+
+try:
+    from filelock import FileLock, Timeout as FileLockTimeout
+except ImportError:
+    FileLock = None
+    FileLockTimeout = None
+from pathlib import Path
+from typing import Any, Iterable, Iterator
+
+
+EXIT_SUCCESS = 0
+EXIT_FALLBACK = 2
+EXIT_ERROR = 1
+
+APPROVED_PREBUILT_LLAMA_TAG = "b8508"
+DEFAULT_LLAMA_TAG = os.environ.get("UNSLOTH_LLAMA_TAG", APPROVED_PREBUILT_LLAMA_TAG)
+DEFAULT_PUBLISHED_REPO = os.environ.get(
+    "UNSLOTH_LLAMA_RELEASE_REPO", "unslothai/llama.cpp"
+)
+DEFAULT_PUBLISHED_TAG = os.environ.get("UNSLOTH_LLAMA_RELEASE_TAG")
+DEFAULT_PUBLISHED_MANIFEST_ASSET = os.environ.get(
+    "UNSLOTH_LLAMA_RELEASE_MANIFEST_ASSET", "llama-prebuilt-manifest.json"
+)
+DEFAULT_PUBLISHED_SHA256_ASSET = os.environ.get(
+    "UNSLOTH_LLAMA_RELEASE_SHA256_ASSET", "llama-prebuilt-sha256.json"
+)
+UPSTREAM_REPO = "ggml-org/llama.cpp"
+UPSTREAM_RELEASES_API = f"https://api.github.com/repos/{UPSTREAM_REPO}/releases/latest"
+TEST_MODEL_URL = (
+    "https://huggingface.co/ggml-org/models/resolve/main/tinyllamas/stories260K.gguf"
+)
+TEST_MODEL_SHA256 = "270cba1bd5109f42d03350f60406024560464db173c0e387d91f0426d3bd256d"
+VALIDATION_MODEL_CACHE_DIRNAME = ".cache"
+VALIDATION_MODEL_CACHE_FILENAME = "stories260K.gguf"
+INSTALL_LOCK_TIMEOUT_SECONDS = 300
+INSTALL_STAGING_ROOT_NAME = ".staging"
+GITHUB_AUTH_HOSTS = {"api.github.com", "github.com"}
+RETRYABLE_HTTP_STATUS = {408, 429, 500, 502, 503, 504}
+HTTP_FETCH_ATTEMPTS = 4
+HTTP_FETCH_BASE_DELAY_SECONDS = 0.75
+SERVER_PORT_BIND_ATTEMPTS = 3
+SERVER_BIND_RETRY_WINDOW_SECONDS = 5.0
+TTY_PROGRESS_START_DELAY_SECONDS = 0.5
+
+
+@dataclass
+class HostInfo:
+    system: str
+    machine: str
+    is_windows: bool
+    is_linux: bool
+    is_macos: bool
+    is_x86_64: bool
+    is_arm64: bool
+    nvidia_smi: str | None
+    driver_cuda_version: tuple[int, int] | None
+    compute_caps: list[str]
+    visible_cuda_devices: str | None
+    has_physical_nvidia: bool
+    has_usable_nvidia: bool
+
+
+@dataclass
+class AssetChoice:
+    repo: str
+    tag: str
+    name: str
+    url: str
+    source_label: str
+    runtime_name: str | None = None
+    runtime_url: str | None = None
+    is_ready_bundle: bool = False
+    install_kind: str = ""
+    bundle_profile: str | None = None
+    runtime_line: str | None = None
+    coverage_class: str | None = None
+    supported_sms: list[str] | None = None
+    min_sm: int | None = None
+    max_sm: int | None = None
+    selection_log: list[str] | None = None
+    expected_sha256: str | None = None
+
+
+@dataclass(frozen = True)
+class PublishedLlamaArtifact:
+    asset_name: str
+    install_kind: str
+    runtime_line: str | None
+    coverage_class: str | None
+    supported_sms: list[str]
+    min_sm: int | None
+    max_sm: int | None
+    bundle_profile: str | None
+    rank: int
+
+
+@dataclass
+class PublishedReleaseBundle:
+    repo: str
+    release_tag: str
+    upstream_tag: str
+    assets: dict[str, str]
+    manifest_asset_name: str
+    artifacts: list[PublishedLlamaArtifact]
+    selection_log: list[str]
+
+
+@dataclass
+class LinuxCudaSelection:
+    attempts: list[AssetChoice]
+    selection_log: list[str]
+
+    @property
+    def primary(self) -> AssetChoice:
+        if not self.attempts:
+            raise RuntimeError("linux CUDA selection unexpectedly had no attempts")
+        return self.attempts[0]
+
+
+@dataclass
+class CudaRuntimePreference:
+    runtime_line: str | None
+    selection_log: list[str]
+
+
+@dataclass(frozen = True)
+class ApprovedArtifactHash:
+    asset_name: str
+    sha256: str
+    repo: str | None
+    kind: str | None
+
+
+@dataclass
+class ApprovedReleaseChecksums:
+    repo: str
+    release_tag: str
+    upstream_tag: str
+    source_commit: str | None
+    artifacts: dict[str, ApprovedArtifactHash]
+
+
+class PrebuiltFallback(RuntimeError):
+    pass
+
+
+def log(message: str) -> None:
+    print(f"[llama-prebuilt] {message}")
+
+
+def log_lines(lines: Iterable[str]) -> None:
+    for line in lines:
+        log(line)
+
+
+def parsed_hostname(url: str | None) -> str | None:
+    if not url:
+        return None
+    try:
+        hostname = urllib.parse.urlparse(url).hostname
+    except Exception:
+        return None
+    if not hostname:
+        return None
+    return hostname.lower()
+
+
+def should_send_github_auth(url: str | None) -> bool:
+    return parsed_hostname(url) in GITHUB_AUTH_HOSTS
+
+
+def auth_headers(url: str | None = None) -> dict[str, str]:
+    headers = {
+        "User-Agent": "unsloth-studio-llama-prebuilt",
+    }
+    token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")
+    if token and should_send_github_auth(url):
+        headers["Authorization"] = f"Bearer {token}"
+    return headers
+
+
+def github_api_headers(url: str | None = None) -> dict[str, str]:
+    return {
+        "Accept": "application/vnd.github+json",
+        **auth_headers(url),
+    }
+
+
+def is_github_api_url(url: str | None) -> bool:
+    return parsed_hostname(url) == "api.github.com"
+
+
+def is_retryable_url_error(exc: Exception) -> bool:
+    if isinstance(exc, urllib.error.HTTPError):
+        return exc.code in RETRYABLE_HTTP_STATUS
+    if isinstance(exc, urllib.error.URLError):
+        return True
+    if isinstance(exc, TimeoutError):
+        return True
+    if isinstance(exc, socket.timeout):
+        return True
+    return False
+
+
+def sleep_backoff(
+    attempt: int, *, base_delay: float = HTTP_FETCH_BASE_DELAY_SECONDS
+) -> None:
+    delay = base_delay * (2 ** max(attempt - 1, 0))
+    delay += random.uniform(0.0, 0.2)
+    time.sleep(delay)
+
+
+def atomic_write_bytes(destination: Path, data: bytes) -> None:
+    destination.parent.mkdir(parents = True, exist_ok = True)
+    with tempfile.NamedTemporaryFile(
+        prefix = destination.name + ".tmp-",
+        dir = destination.parent,
+        delete = False,
+    ) as handle:
+        tmp_path = Path(handle.name)
+        handle.write(data)
+        handle.flush()
+        os.fsync(handle.fileno())
+    os.replace(tmp_path, destination)
+
+
+def atomic_replace_from_tempfile(tmp_path: Path, destination: Path) -> None:
+    destination.parent.mkdir(parents = True, exist_ok = True)
+    os.replace(tmp_path, destination)
+
+
+def source_archive_logical_name(upstream_tag: str) -> str:
+    return f"llama.cpp-source-{upstream_tag}.tar.gz"
+
+
+def sha256_file(path: Path) -> str:
+    digest = hashlib.sha256()
+    with path.open("rb") as handle:
+        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
+            digest.update(chunk)
+    return digest.hexdigest()
+
+
+def normalize_sha256_digest(value: str | None) -> str | None:
+    if not isinstance(value, str) or not value:
+        return None
+    lowered = value.lower()
+    if lowered.startswith("sha256:"):
+        lowered = lowered.split(":", 1)[1]
+    if len(lowered) != 64 or any(ch not in "0123456789abcdef" for ch in lowered):
+        return None
+    return lowered
+
+
+def format_byte_count(num_bytes: float) -> str:
+    units = ["B", "KiB", "MiB", "GiB", "TiB"]
+    value = float(num_bytes)
+    for unit in units:
+        if abs(value) < 1024.0 or unit == units[-1]:
+            if unit == "B":
+                return f"{int(value)} {unit}"
+            return f"{value:.1f} {unit}"
+        value /= 1024.0
+    return f"{num_bytes:.1f} B"
+
+
+class DownloadProgress:
+    def __init__(self, label: str, total_bytes: int | None) -> None:
+        self.label = label
+        self.total_bytes = total_bytes if total_bytes and total_bytes > 0 else None
+        self.start_time = time.monotonic()
+        self.last_emit = 0.0
+        term_ok = os.environ.get("TERM", "").lower() != "dumb"
+        self.stream = (
+            sys.stderr
+            if sys.stderr.isatty()
+            else sys.stdout
+            if sys.stdout.isatty()
+            else sys.stderr
+        )
+        self.is_tty = term_ok and self.stream.isatty()
+        self.completed = False
+        self.last_milestone_percent = -1
+        self.last_milestone_bytes = 0
+        self.has_rendered_tty_progress = False
+
+    def _render(self, downloaded_bytes: int, *, final: bool = False) -> str:
+        elapsed = max(time.monotonic() - self.start_time, 1e-6)
+        speed = downloaded_bytes / elapsed
+        speed_text = f"{format_byte_count(speed)}/s"
+        if self.total_bytes is not None:
+            percent = min(100.0, (downloaded_bytes / self.total_bytes) * 100.0)
+            return (
+                f"{self.label}: {percent:5.1f}% "
+                f"({format_byte_count(downloaded_bytes)}/{format_byte_count(self.total_bytes)}) "
+                f"at {speed_text}"
+            )
+        if final:
+            return f"{self.label}: {format_byte_count(downloaded_bytes)} downloaded at {speed_text}"
+        return f"{self.label}: {format_byte_count(downloaded_bytes)} downloaded at {speed_text}"
+
+    def update(self, downloaded_bytes: int) -> None:
+        now = time.monotonic()
+        if self.is_tty:
+            elapsed = now - self.start_time
+            if not self.has_rendered_tty_progress:
+                if (
+                    self.total_bytes is not None
+                    and downloaded_bytes >= self.total_bytes
+                ):
+                    return
+                if elapsed < TTY_PROGRESS_START_DELAY_SECONDS:
+                    return
+            min_interval = 0.2
+            if (
+                self.has_rendered_tty_progress
+                and not self.completed
+                and (now - self.last_emit) < min_interval
+            ):
+                return
+            self.last_emit = now
+            line = self._render(downloaded_bytes)
+            self.stream.write("\r\033[K" + line)
+            self.stream.flush()
+            self.has_rendered_tty_progress = True
+            return
+
+        should_emit = False
+        if self.total_bytes is not None:
+            percent = int((downloaded_bytes * 100) / max(self.total_bytes, 1))
+            milestone_percent = min((percent // 25) * 25, 100)
+            if (
+                milestone_percent > self.last_milestone_percent
+                and milestone_percent < 100
+            ):
+                self.last_milestone_percent = milestone_percent
+                should_emit = True
+        else:
+            byte_step = 25 * 1024 * 1024
+            if (
+                downloaded_bytes - self.last_milestone_bytes >= byte_step
+                and (now - self.last_emit) >= 5.0
+            ):
+                self.last_milestone_bytes = downloaded_bytes
+                should_emit = True
+
+        if not should_emit:
+            return
+
+        self.last_emit = now
+        self.stream.write(self._render(downloaded_bytes) + "\n")
+        self.stream.flush()
+
+    def finish(self, downloaded_bytes: int) -> None:
+        self.completed = True
+        line = self._render(downloaded_bytes, final = True)
+        if self.is_tty:
+            if not self.has_rendered_tty_progress:
+                return
+            self.stream.write("\r\033[K")
+        else:
+            self.stream.write(line + "\n")
+        self.stream.flush()
+
+
+def download_label_from_url(url: str) -> str:
+    name = Path(urllib.parse.urlparse(url).path).name
+    return name or url
+
+
+def download_bytes(
+    url: str,
+    *,
+    timeout: int = 120,
+    attempts: int = HTTP_FETCH_ATTEMPTS,
+    headers: dict[str, str] | None = None,
+    progress_label: str | None = None,
+) -> bytes:
+    last_exc: Exception | None = None
+    for attempt in range(1, attempts + 1):
+        try:
+            request = urllib.request.Request(url, headers = headers or auth_headers(url))
+            with urllib.request.urlopen(request, timeout = timeout) as response:
+                total_bytes: int | None = None
+                content_length = response.headers.get("Content-Length")
+                if content_length and content_length.isdigit():
+                    total_bytes = int(content_length)
+                progress = (
+                    DownloadProgress(progress_label, total_bytes)
+                    if progress_label
+                    else None
+                )
+                data = bytearray()
+                while True:
+                    chunk = response.read(1024 * 1024)
+                    if not chunk:
+                        break
+                    data.extend(chunk)
+                    if progress is not None:
+                        progress.update(len(data))
+                if progress is not None:
+                    progress.finish(len(data))
+                return bytes(data)
+        except Exception as exc:
+            last_exc = exc
+            if attempt >= attempts or not is_retryable_url_error(exc):
+                raise
+            log(f"fetch failed ({attempt}/{attempts}) for {url}: {exc}; retrying")
+            sleep_backoff(attempt)
+    assert last_exc is not None
+    raise last_exc
+
+
+def fetch_json(url: str) -> Any:
+    data = download_bytes(
+        url,
+        timeout = 30,
+        headers = github_api_headers(url)
+        if is_github_api_url(url)
+        else auth_headers(url),
+    )
+    if not data:
+        raise RuntimeError(f"downloaded empty JSON payload from {url}")
+    try:
+        payload = json.loads(data.decode("utf-8"))
+    except (UnicodeDecodeError, json.JSONDecodeError) as exc:
+        raise RuntimeError(f"downloaded invalid JSON from {url}: {exc}") from exc
+    if not isinstance(payload, dict) and not isinstance(payload, list):
+        raise RuntimeError(
+            f"downloaded unexpected JSON type from {url}: {type(payload).__name__}"
+        )
+    return payload
+
+
+def download_file(url: str, destination: Path) -> None:
+    destination.parent.mkdir(parents = True, exist_ok = True)
+    last_exc: Exception | None = None
+    for attempt in range(1, HTTP_FETCH_ATTEMPTS + 1):
+        tmp_path: Path | None = None
+        try:
+            request = urllib.request.Request(url, headers = auth_headers(url))
+            with tempfile.NamedTemporaryFile(
+                prefix = destination.name + ".tmp-",
+                dir = destination.parent,
+                delete = False,
+            ) as handle:
+                tmp_path = Path(handle.name)
+                with urllib.request.urlopen(request, timeout = 120) as response:
+                    total_bytes: int | None = None
+                    content_length = response.headers.get("Content-Length")
+                    if content_length and content_length.isdigit():
+                        total_bytes = int(content_length)
+                    progress = DownloadProgress(
+                        f"Downloading {destination.name}", total_bytes
+                    )
+                    downloaded_bytes = 0
+                    while True:
+                        chunk = response.read(1024 * 1024)
+                        if not chunk:
+                            break
+                        handle.write(chunk)
+                        downloaded_bytes += len(chunk)
+                        progress.update(downloaded_bytes)
+                    progress.finish(downloaded_bytes)
+                handle.flush()
+                os.fsync(handle.fileno())
+            if not tmp_path.exists() or tmp_path.stat().st_size == 0:
+                raise RuntimeError(f"downloaded empty file from {url}")
+            atomic_replace_from_tempfile(tmp_path, destination)
+            return
+        except Exception as exc:
+            last_exc = exc
+            if tmp_path is not None:
+                try:
+                    tmp_path.unlink(missing_ok = True)
+                except Exception:
+                    pass
+            if attempt >= HTTP_FETCH_ATTEMPTS or not is_retryable_url_error(exc):
+                raise
+            log(
+                f"download failed ({attempt}/{HTTP_FETCH_ATTEMPTS}) for {url}: {exc}; retrying"
+            )
+            sleep_backoff(attempt)
+    assert last_exc is not None
+    raise last_exc
+
+
+def download_file_verified(
+    url: str,
+    destination: Path,
+    *,
+    expected_sha256: str,
+    label: str,
+) -> None:
+    normalized_expected = normalize_sha256_digest(expected_sha256)
+    if not normalized_expected:
+        raise PrebuiltFallback(f"{label} did not have a valid approved sha256")
+
+    for attempt in range(1, 3):
+        download_file(url, destination)
+        actual_sha256 = sha256_file(destination)
+        if actual_sha256 == normalized_expected:
+            log(f"verified {label} sha256={actual_sha256}")
+            return
+
+        log(
+            f"{label} checksum mismatch on attempt {attempt}/2: "
+            f"expected={normalized_expected} actual={actual_sha256}"
+        )
+        destination.unlink(missing_ok = True)
+        if attempt == 2:
+            raise PrebuiltFallback(
+                f"{label} checksum mismatch after retry: expected={normalized_expected} actual={actual_sha256}"
+            )
+        log(f"retrying {label} download after checksum mismatch")
+
+
+def upstream_source_archive_urls(tag: str) -> list[str]:
+    encoded_tag = urllib.parse.quote(tag, safe = "")
+    return [
+        f"https://codeload.github.com/{UPSTREAM_REPO}/tar.gz/refs/tags/{encoded_tag}",
+        f"https://github.com/{UPSTREAM_REPO}/archive/refs/tags/{encoded_tag}.tar.gz",
+    ]
+
+
+def github_release_assets(repo: str, tag: str) -> dict[str, str]:
+    payload = fetch_json(
+        f"https://api.github.com/repos/{repo}/releases/tags/{urllib.parse.quote(tag, safe = '')}"
+    )
+    if not isinstance(payload, dict):
+        raise RuntimeError(f"unexpected release payload for {repo}@{tag}")
+    return release_asset_map(payload)
+
+
+def github_release(repo: str, tag: str) -> dict[str, Any]:
+    payload = fetch_json(
+        f"https://api.github.com/repos/{repo}/releases/tags/{urllib.parse.quote(tag, safe = '')}"
+    )
+    if not isinstance(payload, dict):
+        raise RuntimeError(f"unexpected release payload for {repo}@{tag}")
+    return payload
+
+
+def github_releases(repo: str, *, per_page: int = 100) -> list[dict[str, Any]]:
+    releases: list[dict[str, Any]] = []
+    page = 1
+    while True:
+        payload = fetch_json(
+            f"https://api.github.com/repos/{repo}/releases?per_page={per_page}&page={page}"
+        )
+        if not isinstance(payload, list):
+            raise RuntimeError(f"unexpected releases payload for {repo}")
+        page_items = [item for item in payload if isinstance(item, dict)]
+        releases.extend(page_items)
+        if len(payload) < per_page:
+            break
+        page += 1
+    return releases
+
+
+def latest_upstream_release_tag() -> str:
+    payload = fetch_json(UPSTREAM_RELEASES_API)
+    tag = payload.get("tag_name")
+    if not isinstance(tag, str) or not tag:
+        raise RuntimeError(
+            f"latest release tag was missing from {UPSTREAM_RELEASES_API}"
+        )
+    return tag
+
+
+def normalize_compute_cap(value: Any) -> str | None:
+    raw = str(value).strip()
+    if not raw:
+        return None
+    if "." in raw:
+        parts = raw.split(".", 1)
+        if len(parts) != 2:
+            return None
+        major, minor = parts
+        if not major.isdigit() or not minor.isdigit():
+            return None
+        return f"{int(major)}{int(minor)}"
+    if raw.isdigit():
+        return str(int(raw))
+    return None
+
+
+def normalize_compute_caps(compute_caps: Iterable[str]) -> list[str]:
+    normalized: list[str] = []
+    seen: set[str] = set()
+    for raw in compute_caps:
+        normalized_value = normalize_compute_cap(raw)
+        if normalized_value is None:
+            continue
+        if normalized_value in seen:
+            continue
+        seen.add(normalized_value)
+        normalized.append(normalized_value)
+    normalized.sort(key = int)
+    return normalized
+
+
+def parse_cuda_visible_devices(value: str | None) -> list[str] | None:
+    if value is None:
+        return None
+    raw = value.strip()
+    if not raw or raw == "-1":
+        return []
+    return [token.strip() for token in raw.split(",") if token.strip()]
+
+
+def supports_explicit_visible_device_matching(
+    visible_devices: list[str] | None,
+) -> bool:
+    if not visible_devices:
+        return False
+    for token in visible_devices:
+        lowered = token.lower()
+        if token.isdigit() or lowered.startswith("gpu-"):
+            continue
+        return False
+    return True
+
+
+def select_visible_gpu_rows(
+    gpu_rows: Iterable[tuple[str, str, str]],
+    visible_devices: list[str] | None,
+) -> list[tuple[str, str, str]]:
+    rows = list(gpu_rows)
+    if visible_devices is None:
+        return rows
+    if not visible_devices:
+        return []
+
+    by_index = {index: (index, uuid, cap) for index, uuid, cap in rows}
+    by_uuid = {uuid.lower(): (index, uuid, cap) for index, uuid, cap in rows}
+    selected: list[tuple[str, str, str]] = []
+    seen_indices: set[str] = set()
+    for token in visible_devices:
+        row = by_index.get(token)
+        if row is None:
+            normalized_token = token.lower()
+            row = by_uuid.get(normalized_token)
+            if row is None and normalized_token.startswith("gpu-"):
+                row = by_uuid.get(normalized_token)
+            if row is None and not normalized_token.startswith("gpu-"):
+                row = by_uuid.get("gpu-" + normalized_token)
+        if row is None:
+            continue
+        index = row[0]
+        if index in seen_indices:
+            continue
+        seen_indices.add(index)
+        selected.append(row)
+    return selected
+
+
+def dir_provides_exact_library(directory: str | Path, library: str) -> bool:
+    if not library:
+        return False
+    candidate = Path(directory) / library
+    return candidate.exists() and (candidate.is_file() or candidate.is_symlink())
+
+
+def linux_runtime_dirs_for_required_libraries(
+    required_libraries: Iterable[str],
+) -> list[str]:
+    required = [library for library in required_libraries if library]
+    candidates: list[str | Path] = []
+
+    env_dirs = os.environ.get("CUDA_RUNTIME_LIB_DIR", "")
+    if env_dirs:
+        candidates.extend(part for part in env_dirs.split(os.pathsep) if part)
+    ld_library_path = os.environ.get("LD_LIBRARY_PATH", "")
+    if ld_library_path:
+        candidates.extend(part for part in ld_library_path.split(os.pathsep) if part)
+
+    cuda_roots: list[Path] = []
+    for name in ("CUDA_HOME", "CUDA_PATH", "CUDA_ROOT"):
+        value = os.environ.get(name)
+        if value:
+            cuda_roots.append(Path(value))
+    cuda_roots.extend(
+        Path(path) for path in glob_paths("/usr/local/cuda", "/usr/local/cuda-*")
+    )
+
+    for root in cuda_roots:
+        candidates.extend(
+            [
+                root / "lib",
+                root / "lib64",
+                root / "targets" / "x86_64-linux" / "lib",
+            ]
+        )
+
+    candidates.extend(
+        Path(path)
+        for path in glob_paths(
+            "/lib",
+            "/lib64",
+            "/usr/lib",
+            "/usr/lib64",
+            "/usr/local/lib",
+            "/usr/local/lib64",
+            "/lib/x86_64-linux-gnu",
+            "/usr/lib/x86_64-linux-gnu",
+        )
+    )
+    candidates.extend(
+        Path(path)
+        for path in glob_paths("/usr/local/lib/ollama/cuda_v*", "/usr/lib/wsl/lib")
+    )
+    candidates.extend(Path(path) for path in python_runtime_dirs())
+    candidates.extend(Path(path) for path in ldconfig_runtime_dirs(required))
+
+    resolved = dedupe_existing_dirs(candidates)
+    if not required:
+        return resolved
+
+    matched: list[tuple[int, str]] = []
+    for directory in resolved:
+        base = Path(directory)
+        provided = sum(
+            1 for library in required if dir_provides_exact_library(directory, library)
+        )
+        if provided:
+            matched.append((provided, directory))
+
+    matched.sort(key = lambda item: item[0], reverse = True)
+    return [directory for _, directory in matched]
+
+
+def detected_linux_runtime_lines() -> tuple[list[str], dict[str, list[str]]]:
+    line_requirements = {
+        "cuda13": ["libcudart.so.13", "libcublas.so.13"],
+        "cuda12": ["libcudart.so.12", "libcublas.so.12"],
+    }
+    detected: list[str] = []
+    runtime_dirs: dict[str, list[str]] = {}
+    for line, required in line_requirements.items():
+        dirs = linux_runtime_dirs_for_required_libraries(required)
+        library_matches: dict[str, list[str]] = {}
+        matching_dirs: list[str] = []
+        for library in required:
+            matched_dirs = [
+                directory
+                for directory in dirs
+                if any(Path(directory).glob(f"{library}*"))
+            ]
+            if not matched_dirs:
+                library_matches = {}
+                matching_dirs = []
+                break
+            library_matches[library] = matched_dirs
+            for directory in matched_dirs:
+                if directory not in matching_dirs:
+                    matching_dirs.append(directory)
+        if library_matches:
+            detected.append(line)
+            runtime_dirs[line] = matching_dirs
+    return detected, runtime_dirs
+
+
+def release_asset_map(release: dict[str, Any]) -> dict[str, str]:
+    assets = release.get("assets")
+    if not isinstance(assets, list):
+        return {}
+    return {
+        asset["name"]: asset.get("browser_download_url", "")
+        for asset in assets
+        if isinstance(asset, dict)
+        and isinstance(asset.get("name"), str)
+        and isinstance(asset.get("browser_download_url"), str)
+    }
+
+
+def parse_published_artifact(raw: Any) -> PublishedLlamaArtifact | None:
+    if not isinstance(raw, dict):
+        raise ValueError("artifact entry was not an object")
+    asset_name = raw.get("asset_name")
+    install_kind = raw.get("install_kind")
+    if not isinstance(asset_name, str) or not asset_name:
+        raise ValueError("artifact.asset_name was missing or not a string")
+    if not isinstance(install_kind, str) or not install_kind:
+        raise ValueError(
+            f"artifact {asset_name} install_kind was missing or not a string"
+        )
+
+    supported_sms_raw = raw.get("supported_sms", [])
+    if not isinstance(supported_sms_raw, (list, tuple)):
+        raise ValueError(f"artifact {asset_name} supported_sms must be a list or tuple")
+    if any(not isinstance(value, (int, str)) for value in supported_sms_raw):
+        raise ValueError(
+            f"artifact {asset_name} supported_sms entries must be ints or strings"
+        )
+    supported_sms = normalize_compute_caps(supported_sms_raw)
+
+    min_sm_raw = raw.get("min_sm")
+    max_sm_raw = raw.get("max_sm")
+    try:
+        min_sm = int(min_sm_raw) if min_sm_raw is not None else None
+        max_sm = int(max_sm_raw) if max_sm_raw is not None else None
+    except (TypeError, ValueError) as exc:
+        raise ValueError(
+            f"artifact {asset_name} min_sm/max_sm were not integers"
+        ) from exc
+    runtime_line = raw.get("runtime_line")
+    coverage_class = raw.get("coverage_class")
+    bundle_profile = raw.get("bundle_profile")
+    rank_raw = raw.get("rank", 1000)
+    if runtime_line is not None and not isinstance(runtime_line, str):
+        raise ValueError(f"artifact {asset_name} runtime_line was not a string")
+    if coverage_class is not None and not isinstance(coverage_class, str):
+        raise ValueError(f"artifact {asset_name} coverage_class was not a string")
+    if bundle_profile is not None and not isinstance(bundle_profile, str):
+        raise ValueError(f"artifact {asset_name} bundle_profile was not a string")
+    try:
+        rank = int(rank_raw)
+    except (TypeError, ValueError):
+        raise ValueError(f"artifact {asset_name} rank was not an integer")
+    return PublishedLlamaArtifact(
+        asset_name = asset_name,
+        install_kind = install_kind,
+        runtime_line = runtime_line
+        if isinstance(runtime_line, str) and runtime_line
+        else None,
+        coverage_class = coverage_class
+        if isinstance(coverage_class, str) and coverage_class
+        else None,
+        supported_sms = supported_sms,
+        min_sm = min_sm,
+        max_sm = max_sm,
+        bundle_profile = bundle_profile
+        if isinstance(bundle_profile, str) and bundle_profile
+        else None,
+        rank = rank,
+    )
+
+
+def parse_published_release_bundle(
+    repo: str, release: dict[str, Any]
+) -> PublishedReleaseBundle | None:
+    release_tag = release.get("tag_name")
+    if not isinstance(release_tag, str) or not release_tag:
+        return None
+
+    assets = release_asset_map(release)
+    manifest_url = assets.get(DEFAULT_PUBLISHED_MANIFEST_ASSET)
+    if not manifest_url:
+        return None
+
+    # Mixed repos are filtered by an explicit release-side manifest rather than
+    # by release tag or asset filename conventions.
+    manifest_payload = fetch_json(manifest_url)
+    if not isinstance(manifest_payload, dict):
+        raise RuntimeError(
+            f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} was not a JSON object"
+        )
+    component = manifest_payload.get("component")
+    upstream_tag = manifest_payload.get("upstream_tag")
+    if component != "llama.cpp":
+        return None
+    if not isinstance(upstream_tag, str) or not upstream_tag:
+        raise RuntimeError(
+            f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} in {repo}@{release_tag} omitted upstream_tag"
+        )
+
+    artifacts_payload = manifest_payload.get("artifacts")
+    if not isinstance(artifacts_payload, list):
+        raise RuntimeError(
+            f"published manifest {DEFAULT_PUBLISHED_MANIFEST_ASSET} in {repo}@{release_tag} omitted artifacts"
+        )
+
+    artifacts: list[PublishedLlamaArtifact] = []
+    for index, raw_artifact in enumerate(artifacts_payload):
+        try:
+            artifact = parse_published_artifact(raw_artifact)
+        except ValueError as exc:
+            log(
+                f"published artifact ignored for {repo}@{release_tag} artifact[{index}]: {exc}"
+            )
+            continue
+        if artifact is not None:
+            artifacts.append(artifact)
+    selection_log = [
+        f"published_release: repo={repo}",
+        f"published_release: tag={release_tag}",
+        f"published_release: manifest={DEFAULT_PUBLISHED_MANIFEST_ASSET}",
+        f"published_release: upstream_tag={upstream_tag}",
+    ]
+    return PublishedReleaseBundle(
+        repo = repo,
+        release_tag = release_tag,
+        upstream_tag = upstream_tag,
+        assets = assets,
+        manifest_asset_name = DEFAULT_PUBLISHED_MANIFEST_ASSET,
+        artifacts = artifacts,
+        selection_log = selection_log,
+    )
+
+
+def parse_approved_release_checksums(
+    repo: str,
+    release_tag: str,
+    payload: Any,
+) -> ApprovedReleaseChecksums:
+    if not isinstance(payload, dict):
+        raise RuntimeError(
+            f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} was not a JSON object"
+        )
+    if payload.get("component") != "llama.cpp":
+        raise RuntimeError(
+            f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} did not describe llama.cpp"
+        )
+    payload_release_tag = payload.get("release_tag")
+    if not isinstance(payload_release_tag, str) or not payload_release_tag:
+        raise RuntimeError(
+            f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} omitted release_tag"
+        )
+    if payload_release_tag != release_tag:
+        raise RuntimeError(
+            f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} release_tag={payload_release_tag} "
+            f"did not match pinned release tag {release_tag}"
+        )
+    upstream_tag = payload.get("upstream_tag")
+    if not isinstance(upstream_tag, str) or not upstream_tag:
+        raise RuntimeError(
+            f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} omitted upstream_tag"
+        )
+    artifacts_payload = payload.get("artifacts")
+    if not isinstance(artifacts_payload, dict):
+        raise RuntimeError(
+            f"published checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} omitted artifacts"
+        )
+
+    artifacts: dict[str, ApprovedArtifactHash] = {}
+    for asset_name, raw_entry in artifacts_payload.items():
+        if not isinstance(asset_name, str) or not asset_name:
+            raise RuntimeError(
+                "published checksum asset used a non-string artifact key"
+            )
+        if not isinstance(raw_entry, dict):
+            raise RuntimeError(
+                f"published checksum entry for {asset_name} was not an object"
+            )
+        digest = normalize_sha256_digest(raw_entry.get("sha256"))
+        if not digest:
+            raise RuntimeError(
+                f"published checksum entry for {asset_name} omitted a valid sha256"
+            )
+        repo_value = raw_entry.get("repo")
+        kind_value = raw_entry.get("kind")
+        artifacts[asset_name] = ApprovedArtifactHash(
+            asset_name = asset_name,
+            sha256 = digest,
+            repo = repo_value if isinstance(repo_value, str) and repo_value else None,
+            kind = kind_value if isinstance(kind_value, str) and kind_value else None,
+        )
+
+    source_commit = payload.get("source_commit")
+    return ApprovedReleaseChecksums(
+        repo = repo,
+        release_tag = release_tag,
+        upstream_tag = upstream_tag,
+        source_commit = source_commit
+        if isinstance(source_commit, str) and source_commit
+        else None,
+        artifacts = artifacts,
+    )
+
+
+def load_approved_release_checksums(
+    repo: str, release_tag: str
+) -> ApprovedReleaseChecksums:
+    try:
+        release = github_release(repo, release_tag)
+    except Exception as exc:
+        raise PrebuiltFallback(
+            f"approved prebuilt release {repo}@{release_tag} was not available"
+        ) from exc
+    assets = release_asset_map(release)
+    checksum_url = assets.get(DEFAULT_PUBLISHED_SHA256_ASSET)
+    if not checksum_url:
+        raise PrebuiltFallback(
+            f"approved prebuilt release {repo}@{release_tag} did not expose {DEFAULT_PUBLISHED_SHA256_ASSET}"
+        )
+    try:
+        payload = fetch_json(checksum_url)
+        checksums = parse_approved_release_checksums(repo, release_tag, payload)
+    except PrebuiltFallback:
+        raise
+    except Exception as exc:
+        raise PrebuiltFallback(
+            f"approved checksum asset {DEFAULT_PUBLISHED_SHA256_ASSET} in {repo}@{release_tag} was invalid"
+        ) from exc
+    return checksums
+
+
+def iter_published_release_bundles(
+    repo: str, published_release_tag: str = ""
+) -> Iterable[PublishedReleaseBundle]:
+    releases = (
+        [github_release(repo, published_release_tag)]
+        if published_release_tag
+        else github_releases(repo)
+    )
+    for release in releases:
+        if not published_release_tag and (
+            release.get("draft") or release.get("prerelease")
+        ):
+            continue
+        try:
+            bundle = parse_published_release_bundle(repo, release)
+        except Exception as exc:
+            release_tag = release.get("tag_name", "unknown")
+            log(f"published release metadata ignored for {repo}@{release_tag}: {exc}")
+            continue
+        if bundle is None:
+            continue
+        yield bundle
+
+
+def linux_cuda_choice_from_release(
+    host: HostInfo,
+    release: PublishedReleaseBundle,
+    preferred_runtime_line: str | None = None,
+    selection_preamble: Iterable[str] = (),
+) -> LinuxCudaSelection | None:
+    host_sms = normalize_compute_caps(host.compute_caps)
+    detected_runtime_lines, runtime_dirs = detected_linux_runtime_lines()
+    driver_runtime_lines = compatible_linux_runtime_lines(host)
+    runtime_lines = [
+        runtime_line
+        for runtime_line in detected_runtime_lines
+        if runtime_line in driver_runtime_lines
+    ]
+    ordered_runtime_lines = list(runtime_lines)
+    selection_log = (
+        list(release.selection_log)
+        + list(selection_preamble)
+        + [
+            f"linux_cuda_selection: release={release.release_tag}",
+            f"linux_cuda_selection: detected_sms={','.join(host_sms) if host_sms else 'unknown'}",
+            "linux_cuda_selection: detected_runtime_lines="
+            + (",".join(detected_runtime_lines) if detected_runtime_lines else "none"),
+            "linux_cuda_selection: driver_runtime_lines="
+            + (",".join(driver_runtime_lines) if driver_runtime_lines else "none"),
+            "linux_cuda_selection: compatible_runtime_lines="
+            + (",".join(runtime_lines) if runtime_lines else "none"),
+        ]
+    )
+    for runtime_line in ("cuda13", "cuda12"):
+        selection_log.append(
+            "linux_cuda_selection: runtime_dirs "
+            f"{runtime_line}="
+            + (
+                ",".join(runtime_dirs.get(runtime_line, []))
+                if runtime_dirs.get(runtime_line)
+                else "none"
+            )
+        )
+    published_artifacts = [
+        artifact
+        for artifact in release.artifacts
+        if artifact.install_kind == "linux-cuda"
+    ]
+    published_asset_names = sorted(
+        artifact.asset_name for artifact in published_artifacts
+    )
+    selection_log.append(
+        "linux_cuda_selection: published_assets="
+        + (",".join(published_asset_names) if published_asset_names else "none")
+    )
+
+    if not host_sms:
+        selection_log.append(
+            "linux_cuda_selection: compute capability detection unavailable; prefer portable by runtime line"
+        )
+    if not runtime_lines:
+        selection_log.append(
+            "linux_cuda_selection: no Linux CUDA runtime line satisfied both runtime libraries and driver compatibility"
+        )
+        return None
+
+    if preferred_runtime_line:
+        if preferred_runtime_line in ordered_runtime_lines:
+            ordered_runtime_lines = [preferred_runtime_line] + [
+                runtime_line
+                for runtime_line in ordered_runtime_lines
+                if runtime_line != preferred_runtime_line
+            ]
+            selection_log.append(
+                "linux_cuda_selection: torch_preferred_runtime_line="
+                f"{preferred_runtime_line} reordered_attempts={','.join(ordered_runtime_lines)}"
+            )
+        else:
+            selection_log.append(
+                "linux_cuda_selection: torch_preferred_runtime_line="
+                f"{preferred_runtime_line} unavailable_on_host"
+            )
+
+    attempts: list[AssetChoice] = []
+    seen_attempts: set[str] = set()
+
+    def add_attempt(
+        artifact: PublishedLlamaArtifact, asset_url: str, reason: str
+    ) -> None:
+        asset_name = artifact.asset_name
+        if asset_name in seen_attempts:
+            return
+        seen_attempts.add(asset_name)
+        attempts.append(
+            AssetChoice(
+                repo = release.repo,
+                tag = release.release_tag,
+                name = asset_name,
+                url = asset_url,
+                source_label = "published",
+                is_ready_bundle = True,
+                install_kind = "linux-cuda",
+                bundle_profile = artifact.bundle_profile,
+                runtime_line = artifact.runtime_line,
+                coverage_class = artifact.coverage_class,
+                supported_sms = artifact.supported_sms,
+                min_sm = artifact.min_sm,
+                max_sm = artifact.max_sm,
+                selection_log = list(selection_log)
+                + [
+                    "linux_cuda_selection: selected "
+                    f"{asset_name} runtime_line={artifact.runtime_line} coverage_class={artifact.coverage_class} reason={reason}"
+                ],
+            )
+        )
+
+    for runtime_line in ordered_runtime_lines:
+        coverage_candidates: list[tuple[PublishedLlamaArtifact, str]] = []
+        portable_candidate: tuple[PublishedLlamaArtifact, str] | None = None
+        for artifact in published_artifacts:
+            if artifact.runtime_line != runtime_line:
+                continue
+            asset_name = artifact.asset_name
+            asset_url = release.assets.get(asset_name)
+            if not asset_url:
+                selection_log.append(
+                    f"linux_cuda_selection: reject {asset_name} missing asset"
+                )
+                continue
+            if not host_sms and artifact.coverage_class != "portable":
+                selection_log.append(
+                    "linux_cuda_selection: reject "
+                    f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} "
+                    "reason=unknown_compute_caps_prefer_portable"
+                )
+                continue
+
+            if not artifact.supported_sms:
+                selection_log.append(
+                    "linux_cuda_selection: reject "
+                    f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} "
+                    "reason=artifact_missing_supported_sms"
+                )
+                continue
+            if artifact.min_sm is None or artifact.max_sm is None:
+                selection_log.append(
+                    "linux_cuda_selection: reject "
+                    f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} "
+                    "reason=artifact_missing_sm_bounds"
+                )
+                continue
+
+            supported_sms = {str(value) for value in artifact.supported_sms}
+            missing_sms = [sm for sm in host_sms if sm not in supported_sms]
+            out_of_range_sms = [
+                sm
+                for sm in host_sms
+                if not (artifact.min_sm <= int(sm) <= artifact.max_sm)
+            ]
+            reasons: list[str] = []
+            if missing_sms:
+                reasons.append(f"missing_sms={','.join(missing_sms)}")
+            if out_of_range_sms:
+                reasons.append(f"out_of_range_sms={','.join(out_of_range_sms)}")
+            if reasons:
+                selection_log.append(
+                    "linux_cuda_selection: reject "
+                    f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} "
+                    f"coverage={artifact.min_sm}-{artifact.max_sm} supported={','.join(artifact.supported_sms)} "
+                    f"reasons={' '.join(reasons)}"
+                )
+                continue
+
+            selection_log.append(
+                "linux_cuda_selection: accept "
+                f"{asset_name} runtime_line={runtime_line} coverage_class={artifact.coverage_class} "
+                f"coverage={artifact.min_sm}-{artifact.max_sm} supported={','.join(artifact.supported_sms)}"
+            )
+            if artifact.coverage_class == "portable":
+                portable_candidate = (artifact, asset_url)
+            else:
+                coverage_candidates.append((artifact, asset_url))
+
+        if coverage_candidates:
+            artifact, url = sorted(
+                coverage_candidates,
+                key = lambda item: (
+                    (item[0].max_sm or 0) - (item[0].min_sm or 0),
+                    item[0].rank,
+                    item[0].max_sm or 0,
+                ),
+            )[0]
+            add_attempt(artifact, url, "best coverage for runtime line")
+        if portable_candidate:
+            artifact, url = portable_candidate
+            add_attempt(artifact, url, "portable fallback for runtime line")
+
+    if not attempts:
+        return None
+
+    selection_log.append(
+        "linux_cuda_selection: attempt_order="
+        + ",".join(choice.name for choice in attempts)
+    )
+    for attempt in attempts:
+        attempt.selection_log = list(selection_log) + [
+            "linux_cuda_selection: attempt "
+            f"{attempt.name} runtime_line={attempt.runtime_line} coverage_class={attempt.coverage_class}"
+        ]
+    return LinuxCudaSelection(attempts = attempts, selection_log = selection_log)
+
+
+def latest_published_linux_cuda_tag(host: HostInfo, published_repo: str) -> str | None:
+    for release in iter_published_release_bundles(published_repo):
+        if linux_cuda_choice_from_release(host, release):
+            return release.upstream_tag
+    return None
+
+
+def iter_upstream_releases() -> Iterable[dict[str, Any]]:
+    for release in github_releases(UPSTREAM_REPO):
+        if release.get("draft") or release.get("prerelease"):
+            continue
+        yield release
+
+
+def pinned_published_release_bundle(
+    repo: str, published_release_tag: str
+) -> PublishedReleaseBundle:
+    bundle = next(iter_published_release_bundles(repo, published_release_tag), None)
+    if bundle is None:
+        raise PrebuiltFallback(
+            f"published release {repo}@{published_release_tag} did not expose a usable llama.cpp manifest"
+        )
+    return bundle
+
+
+def resolve_requested_llama_tag(
+    requested_tag: str | None,
+) -> str:
+    if requested_tag and requested_tag != "latest":
+        return requested_tag
+    return latest_upstream_release_tag()
+
+
+def resolve_requested_install_tag(
+    requested_tag: str | None,
+    published_release_tag: str = "",
+) -> str:
+    approved_tag = APPROVED_PREBUILT_LLAMA_TAG
+    normalized_requested = requested_tag or "latest"
+    if normalized_requested not in {"latest", approved_tag}:
+        raise PrebuiltFallback(
+            f"prebuilt installs are pinned to approved release {approved_tag}; requested {normalized_requested}"
+        )
+    if published_release_tag and published_release_tag != approved_tag:
+        raise PrebuiltFallback(
+            f"prebuilt installs require published release tag {approved_tag}; requested {published_release_tag}"
+        )
+    return approved_tag
+
+
+def run_capture(
+    command: list[str],
+    *,
+    timeout: int = 30,
+    check: bool = False,
+    env: dict[str, str] | None = None,
+) -> subprocess.CompletedProcess[str]:
+    result = subprocess.run(
+        command,
+        capture_output = True,
+        text = True,
+        timeout = timeout,
+        env = env,
+    )
+    if check and result.returncode != 0:
+        raise subprocess.CalledProcessError(
+            result.returncode, command, result.stdout, result.stderr
+        )
+    return result
+
+
+def detect_host() -> HostInfo:
+    system = platform.system()
+    machine = platform.machine().lower()
+    is_windows = system == "Windows"
+    is_linux = system == "Linux"
+    is_macos = system == "Darwin"
+    is_x86_64 = machine in {"x86_64", "amd64"}
+    is_arm64 = machine in {"arm64", "aarch64"}
+
+    nvidia_smi = shutil.which("nvidia-smi")
+    driver_cuda_version = None
+    compute_caps: list[str] = []
+    visible_cuda_devices = os.environ.get("CUDA_VISIBLE_DEVICES")
+    visible_device_tokens = parse_cuda_visible_devices(visible_cuda_devices)
+    has_physical_nvidia = False
+    has_usable_nvidia = False
+    if nvidia_smi:
+        try:
+            result = run_capture([nvidia_smi], timeout = 20)
+            merged = "\n".join(part for part in (result.stdout, result.stderr) if part)
+            if "NVIDIA-SMI" in merged:
+                has_physical_nvidia = True
+                has_usable_nvidia = visible_device_tokens != []
+            for line in merged.splitlines():
+                if "CUDA Version:" in line:
+                    raw = line.split("CUDA Version:", 1)[1].strip().split()[0]
+                    major, minor = raw.split(".", 1)
+                    driver_cuda_version = (int(major), int(minor))
+                    break
+        except Exception:
+            pass
+
+        try:
+            caps = run_capture(
+                [
+                    nvidia_smi,
+                    "--query-gpu=index,uuid,compute_cap",
+                    "--format=csv,noheader",
+                ],
+                timeout = 20,
+            )
+            visible_gpu_rows: list[tuple[str, str, str]] = []
+            for raw in caps.stdout.splitlines():
+                parts = [part.strip() for part in raw.split(",")]
+                if len(parts) != 3:
+                    continue
+                index, uuid, cap = parts
+                visible_gpu_row = select_visible_gpu_rows(
+                    [(index, uuid, cap)],
+                    visible_device_tokens,
+                )
+                if not visible_gpu_row:
+                    continue
+                visible_gpu_rows.extend(visible_gpu_row)
+                normalized_cap = normalize_compute_cap(cap)
+                if normalized_cap is None:
+                    continue
+                if normalized_cap not in compute_caps:
+                    compute_caps.append(normalized_cap)
+
+            if visible_gpu_rows:
+                has_usable_nvidia = True
+            elif visible_device_tokens == []:
+                has_usable_nvidia = False
+            elif supports_explicit_visible_device_matching(visible_device_tokens):
+                has_usable_nvidia = False
+            elif has_physical_nvidia:
+                has_usable_nvidia = True
+        except Exception:
+            pass
+
+    return HostInfo(
+        system = system,
+        machine = machine,
+        is_windows = is_windows,
+        is_linux = is_linux,
+        is_macos = is_macos,
+        is_x86_64 = is_x86_64,
+        is_arm64 = is_arm64,
+        nvidia_smi = nvidia_smi,
+        driver_cuda_version = driver_cuda_version,
+        compute_caps = compute_caps,
+        visible_cuda_devices = visible_cuda_devices,
+        has_physical_nvidia = has_physical_nvidia,
+        has_usable_nvidia = has_usable_nvidia,
+    )
+
+
+def pick_windows_cuda_runtime(host: HostInfo) -> str | None:
+    if not host.driver_cuda_version:
+        return None
+    major, minor = host.driver_cuda_version
+    if major > 13 or (major == 13 and minor >= 1):
+        return "13.1"
+    if major > 12 or (major == 12 and minor >= 4):
+        return "12.4"
+    return None
+
+
+def compatible_linux_runtime_lines(host: HostInfo) -> list[str]:
+    if not host.driver_cuda_version:
+        return []
+    major, _minor = host.driver_cuda_version
+    if major >= 13:
+        return ["cuda13", "cuda12"]
+    if major >= 12:
+        return ["cuda12"]
+    return []
+
+
+def windows_runtime_line_info() -> dict[str, tuple[str, ...]]:
+    return {
+        "cuda13": ("cudart64_13*.dll", "cublas64_13*.dll", "cublasLt64_13*.dll"),
+        "cuda12": ("cudart64_12*.dll", "cublas64_12*.dll", "cublasLt64_12*.dll"),
+    }
+
+
+def detected_windows_runtime_lines() -> tuple[list[str], dict[str, list[str]]]:
+    dirs = windows_runtime_dirs()
+    detected: list[str] = []
+    runtime_dirs: dict[str, list[str]] = {}
+    for runtime_line, required_patterns in windows_runtime_line_info().items():
+        matching_dirs = windows_runtime_dirs_for_patterns(required_patterns, dirs)
+        if matching_dirs:
+            detected.append(runtime_line)
+            runtime_dirs[runtime_line] = matching_dirs
+    return detected, runtime_dirs
+
+
+def compatible_windows_runtime_lines(host: HostInfo) -> list[str]:
+    driver_runtime = pick_windows_cuda_runtime(host)
+    if driver_runtime == "13.1":
+        return ["cuda13", "cuda12"]
+    if driver_runtime == "12.4":
+        return ["cuda12"]
+    return []
+
+
+def runtime_line_from_cuda_version(cuda_version: str | None) -> str | None:
+    if not cuda_version:
+        return None
+    raw = str(cuda_version).strip()
+    if not raw:
+        return None
+    major, _, _ = raw.partition(".")
+    if major == "12":
+        return "cuda12"
+    if major == "13":
+        return "cuda13"
+    return None
+
+
+def detect_torch_cuda_runtime_preference(host: HostInfo) -> CudaRuntimePreference:
+    selection_log: list[str] = []
+    if host.is_macos:
+        selection_log.append("torch_cuda_preference: skipped on macOS")
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+    if not (host.has_usable_nvidia and (host.is_linux or host.is_windows)):
+        selection_log.append(
+            "torch_cuda_preference: skipped because CUDA host prerequisites were not met"
+        )
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+
+    try:
+        import torch
+    except Exception as exc:
+        selection_log.append(f"torch_cuda_preference: import failed: {exc}")
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+
+    cuda_version = getattr(getattr(torch, "version", None), "cuda", None)
+    if not isinstance(cuda_version, str) or not cuda_version.strip():
+        selection_log.append(
+            "torch_cuda_preference: torch.version.cuda missing; skipping Torch shortcut"
+        )
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+
+    try:
+        cuda_available = bool(torch.cuda.is_available())
+    except Exception as exc:
+        selection_log.append(
+            f"torch_cuda_preference: torch.cuda.is_available() failed: {exc}"
+        )
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+
+    if not cuda_available:
+        selection_log.append(
+            "torch_cuda_preference: torch.cuda.is_available() returned False; falling back to normal selection"
+        )
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+
+    runtime_line = runtime_line_from_cuda_version(cuda_version)
+    if runtime_line is None:
+        selection_log.append(
+            f"torch_cuda_preference: unsupported torch.version.cuda={cuda_version}; falling back to normal selection"
+        )
+        return CudaRuntimePreference(runtime_line = None, selection_log = selection_log)
+
+    selection_log.append(
+        "torch_cuda_preference: selected runtime_line="
+        f"{runtime_line} from torch.version.cuda={cuda_version}"
+    )
+    return CudaRuntimePreference(runtime_line = runtime_line, selection_log = selection_log)
+
+
+def windows_cuda_attempts(
+    host: HostInfo,
+    llama_tag: str,
+    upstream_assets: dict[str, str],
+    preferred_runtime_line: str | None,
+    selection_preamble: Iterable[str] = (),
+) -> list[AssetChoice]:
+    selection_log = list(selection_preamble)
+    runtime_by_line = {"cuda12": "12.4", "cuda13": "13.1"}
+    driver_runtime = pick_windows_cuda_runtime(host)
+    detected_runtime_lines, runtime_dirs = detected_windows_runtime_lines()
+    compatible_runtime_lines = compatible_windows_runtime_lines(host)
+    normal_runtime_lines: list[str]
+    if detected_runtime_lines:
+        normal_runtime_lines = [
+            line for line in compatible_runtime_lines if line in detected_runtime_lines
+        ]
+    else:
+        normal_runtime_lines = compatible_runtime_lines
+    selection_log.append(
+        "windows_cuda_selection: driver_runtime="
+        + (driver_runtime if driver_runtime else "unknown")
+    )
+    selection_log.append(
+        "windows_cuda_selection: detected_runtime_lines="
+        + (",".join(detected_runtime_lines) if detected_runtime_lines else "none")
+    )
+    for runtime_line in ("cuda13", "cuda12"):
+        selection_log.append(
+            "windows_cuda_selection: runtime_dirs "
+            f"{runtime_line}="
+            + (
+                ",".join(runtime_dirs.get(runtime_line, []))
+                if runtime_dirs.get(runtime_line)
+                else "none"
+            )
+        )
+    if detected_runtime_lines:
+        selection_log.append(
+            "windows_cuda_selection: host_runtime_order="
+            + (",".join(normal_runtime_lines) if normal_runtime_lines else "none")
+        )
+    else:
+        selection_log.append(
+            "windows_cuda_selection: no CUDA runtime DLL line detected; falling back to driver order"
+        )
+    if not normal_runtime_lines:
+        if detected_runtime_lines:
+            selection_log.append(
+                "windows_cuda_selection: detected CUDA runtime DLLs were incompatible with the reported driver"
+            )
+        fallback_runtime_lines = (
+            ["cuda13", "cuda12"]
+            if driver_runtime == "13.1"
+            else (["cuda12"] if driver_runtime == "12.4" else [])
+        )
+        normal_runtime_lines = fallback_runtime_lines
+
+    runtime_order: list[str] = []
+    if preferred_runtime_line and preferred_runtime_line in normal_runtime_lines:
+        runtime_order.append(preferred_runtime_line)
+        selection_log.append(
+            "windows_cuda_selection: torch_preferred_runtime_line="
+            f"{preferred_runtime_line} reordered_attempts"
+        )
+    elif preferred_runtime_line:
+        selection_log.append(
+            "windows_cuda_selection: torch_preferred_runtime_line="
+            f"{preferred_runtime_line} unavailable_or_incompatible"
+        )
+    else:
+        selection_log.append(
+            "windows_cuda_selection: no Torch runtime preference available"
+        )
+
+    runtime_order.extend(
+        runtime_line
+        for runtime_line in normal_runtime_lines
+        if runtime_line not in runtime_order
+    )
+    selection_log.append(
+        "windows_cuda_selection: normal_runtime_order="
+        + (",".join(normal_runtime_lines) if normal_runtime_lines else "none")
+    )
+    selection_log.append(
+        "windows_cuda_selection: attempt_runtime_order="
+        + (",".join(runtime_order) if runtime_order else "none")
+    )
+
+    attempts: list[AssetChoice] = []
+    for runtime_line in runtime_order:
+        runtime = runtime_by_line[runtime_line]
+        upstream_name = f"llama-{llama_tag}-bin-win-cuda-{runtime}-x64.zip"
+        asset_url = upstream_assets.get(upstream_name)
+        if not asset_url:
+            selection_log.append(
+                f"windows_cuda_selection: skip missing asset {upstream_name}"
+            )
+            continue
+        attempts.append(
+            AssetChoice(
+                repo = UPSTREAM_REPO,
+                tag = llama_tag,
+                name = upstream_name,
+                url = asset_url,
+                source_label = "upstream",
+                install_kind = "windows-cuda",
+                runtime_line = runtime_line,
+                selection_log = list(selection_log)
+                + [
+                    f"windows_cuda_selection: selected {upstream_name} runtime={runtime}"
+                ],
+            )
+        )
+    return attempts
+
+
+def resolve_windows_cuda_choices(
+    host: HostInfo, llama_tag: str, upstream_assets: dict[str, str]
+) -> list[AssetChoice]:
+    torch_preference = detect_torch_cuda_runtime_preference(host)
+    attempts = windows_cuda_attempts(
+        host,
+        llama_tag,
+        upstream_assets,
+        torch_preference.runtime_line,
+        torch_preference.selection_log,
+    )
+    return attempts
+
+
+def resolve_linux_cuda_choice(
+    host: HostInfo, llama_tag: str, published_repo: str, published_release_tag: str
+) -> LinuxCudaSelection:
+    torch_preference = detect_torch_cuda_runtime_preference(host)
+    skipped_tag_mismatches = 0
+    for release in iter_published_release_bundles(
+        published_repo, published_release_tag
+    ):
+        if release.upstream_tag != llama_tag:
+            skipped_tag_mismatches += 1
+            continue
+        selection = linux_cuda_choice_from_release(
+            host,
+            release,
+            preferred_runtime_line = torch_preference.runtime_line,
+            selection_preamble = torch_preference.selection_log,
+        )
+        if selection is not None:
+            return selection
+    if skipped_tag_mismatches:
+        log(
+            "published Linux CUDA selection skipped "
+            f"{skipped_tag_mismatches} release(s) with upstream_tag != {llama_tag}"
+        )
+    raise PrebuiltFallback("no compatible published Linux CUDA bundle was found")
+
+
+def resolve_upstream_asset_choice(host: HostInfo, llama_tag: str) -> AssetChoice:
+    upstream_assets = github_release_assets(UPSTREAM_REPO, llama_tag)
+    if host.is_linux and host.is_x86_64:
+        upstream_name = f"llama-{llama_tag}-bin-ubuntu-x64.tar.gz"
+        if upstream_name not in upstream_assets:
+            raise PrebuiltFallback("upstream Linux CPU asset was not found")
+        return AssetChoice(
+            repo = UPSTREAM_REPO,
+            tag = llama_tag,
+            name = upstream_name,
+            url = upstream_assets[upstream_name],
+            source_label = "upstream",
+            install_kind = "linux-cpu",
+        )
+
+    if host.is_windows and host.is_x86_64:
+        if host.has_usable_nvidia:
+            attempts = resolve_windows_cuda_choices(host, llama_tag, upstream_assets)
+            if attempts:
+                return attempts[0]
+            raise PrebuiltFallback("no compatible Windows CUDA asset was found")
+
+        upstream_name = f"llama-{llama_tag}-bin-win-cpu-x64.zip"
+        if upstream_name not in upstream_assets:
+            raise PrebuiltFallback("upstream Windows CPU asset was not found")
+        return AssetChoice(
+            repo = UPSTREAM_REPO,
+            tag = llama_tag,
+            name = upstream_name,
+            url = upstream_assets[upstream_name],
+            source_label = "upstream",
+            install_kind = "windows-cpu",
+        )
+
+    if host.is_macos and host.is_arm64:
+        upstream_name = f"llama-{llama_tag}-bin-macos-arm64.tar.gz"
+        if upstream_name not in upstream_assets:
+            raise PrebuiltFallback("upstream macOS arm64 asset was not found")
+        return AssetChoice(
+            repo = UPSTREAM_REPO,
+            tag = llama_tag,
+            name = upstream_name,
+            url = upstream_assets[upstream_name],
+            source_label = "upstream",
+            install_kind = "macos-arm64",
+        )
+
+    if host.is_macos and host.is_x86_64:
+        upstream_name = f"llama-{llama_tag}-bin-macos-x64.tar.gz"
+        if upstream_name not in upstream_assets:
+            raise PrebuiltFallback("upstream macOS x64 asset was not found")
+        return AssetChoice(
+            repo = UPSTREAM_REPO,
+            tag = llama_tag,
+            name = upstream_name,
+            url = upstream_assets[upstream_name],
+            source_label = "upstream",
+            install_kind = "macos-x64",
+        )
+
+    raise PrebuiltFallback(
+        f"no prebuilt policy exists for {host.system} {host.machine}"
+    )
+
+
+def resolve_asset_choice(
+    host: HostInfo, llama_tag: str, published_repo: str, published_release_tag: str
+) -> AssetChoice:
+    if host.is_linux and host.is_x86_64 and host.has_usable_nvidia:
+        return resolve_linux_cuda_choice(
+            host, llama_tag, published_repo, published_release_tag
+        ).primary
+    return resolve_upstream_asset_choice(host, llama_tag)
+
+
+def extract_archive(archive_path: Path, destination: Path) -> None:
+    def safe_extract_path(base: Path, member_name: str) -> Path:
+        normalized = member_name.replace("\\", "/")
+        member_path = Path(normalized)
+        if member_path.is_absolute():
+            raise PrebuiltFallback(
+                f"archive member used an absolute path: {member_name}"
+            )
+
+        target = (base / member_path).resolve()
+        base_resolved = base.resolve()
+        try:
+            target.relative_to(base_resolved)
+        except ValueError as exc:
+            raise PrebuiltFallback(
+                f"archive member escaped destination: {member_name}"
+            ) from exc
+        return target
+
+    def safe_link_target(
+        base: Path, member_name: str, link_name: str, target: Path
+    ) -> tuple[str, Path]:
+        normalized = link_name.replace("\\", "/")
+        link_path = Path(normalized)
+        if link_path.is_absolute():
+            raise PrebuiltFallback(
+                f"archive link used an absolute target: {member_name} -> {link_name}"
+            )
+        if not normalized:
+            raise PrebuiltFallback(f"archive link used an empty target: {member_name}")
+
+        resolved = (target.parent / link_path).resolve()
+        base_resolved = base.resolve()
+        try:
+            resolved.relative_to(base_resolved)
+        except ValueError as exc:
+            raise PrebuiltFallback(
+                f"archive link escaped destination: {member_name} -> {link_name}"
+            ) from exc
+        return normalized, resolved
+
+    def extract_zip_safely(source: Path, base: Path) -> None:
+        with zipfile.ZipFile(source) as archive:
+            for member in archive.infolist():
+                target = safe_extract_path(base, member.filename)
+                mode = (member.external_attr >> 16) & 0o170000
+                if mode == 0o120000:
+                    raise PrebuiltFallback(
+                        f"zip archive contained a symlink entry: {member.filename}"
+                    )
+                if member.is_dir():
+                    target.mkdir(parents = True, exist_ok = True)
+                    continue
+                target.parent.mkdir(parents = True, exist_ok = True)
+                with archive.open(member, "r") as src, target.open("wb") as dst:
+                    shutil.copyfileobj(src, dst)
+
+    def extract_tar_safely(source: Path, base: Path) -> None:
+        pending_links: list[tuple[tarfile.TarInfo, Path]] = []
+        with tarfile.open(source, "r:gz") as archive:
+            for member in archive.getmembers():
+                target = safe_extract_path(base, member.name)
+                if member.isdir():
+                    target.mkdir(parents = True, exist_ok = True)
+                    continue
+                if member.islnk() or member.issym():
+                    pending_links.append((member, target))
+                    continue
+                if not member.isfile():
+                    raise PrebuiltFallback(
+                        f"tar archive contained an unsupported entry: {member.name}"
+                    )
+                target.parent.mkdir(parents = True, exist_ok = True)
+                extracted = archive.extractfile(member)
+                if extracted is None:
+                    raise PrebuiltFallback(
+                        f"tar archive entry could not be read: {member.name}"
+                    )
+                with extracted, target.open("wb") as dst:
+                    shutil.copyfileobj(extracted, dst)
+
+        unresolved = list(pending_links)
+        while unresolved:
+            next_round: list[tuple[tarfile.TarInfo, Path]] = []
+            progressed = False
+            for member, target in unresolved:
+                normalized_link, resolved_target = safe_link_target(
+                    base, member.name, member.linkname, target
+                )
+                if not resolved_target.exists() and not resolved_target.is_symlink():
+                    next_round.append((member, target))
+                    continue
+                if resolved_target.is_dir():
+                    raise PrebuiltFallback(
+                        f"archive link targeted a directory: {member.name} -> {member.linkname}"
+                    )
+
+                target.parent.mkdir(parents = True, exist_ok = True)
+                if target.exists() or target.is_symlink():
+                    target.unlink()
+
+                if member.issym():
+                    target.symlink_to(normalized_link)
+                else:
+                    shutil.copy2(resolved_target, target)
+                progressed = True
+
+            if not progressed:
+                details = ", ".join(
+                    f"{member.name} -> {member.linkname}" for member, _ in next_round
+                )
+                raise PrebuiltFallback(
+                    f"tar archive contained unresolved link entries: {details}"
+                )
+            unresolved = next_round
+
+    destination.mkdir(parents = True, exist_ok = True)
+    if archive_path.name.endswith(".zip"):
+        extract_zip_safely(archive_path, destination)
+        return
+    if archive_path.name.endswith(".tar.gz"):
+        extract_tar_safely(archive_path, destination)
+        return
+    raise PrebuiltFallback(f"unsupported archive format: {archive_path.name}")
+
+
+def copy_globs(
+    source_dir: Path, destination: Path, patterns: list[str], *, required: bool = True
+) -> None:
+    destination.mkdir(parents = True, exist_ok = True)
+    matched_sources: dict[str, Path] = {}
+    for path in sorted(
+        (candidate for candidate in source_dir.rglob("*") if candidate.is_file()),
+        key = lambda candidate: (
+            len(candidate.relative_to(source_dir).parts),
+            str(candidate),
+        ),
+    ):
+        for pattern in patterns:
+            if fnmatch.fnmatch(path.name, pattern):
+                previous = matched_sources.get(path.name)
+                if previous is not None and previous != path:
+                    raise PrebuiltFallback(
+                        f"ambiguous archive layout for {path.name}: "
+                        f"{previous.relative_to(source_dir)} and {path.relative_to(source_dir)}"
+                    )
+                matched_sources[path.name] = path
+                break
+
+    if required and not matched_sources:
+        raise PrebuiltFallback(f"required files missing from {source_dir}: {patterns}")
+
+    for name, path in matched_sources.items():
+        shutil.copy2(path, destination / name)
+
+
+def ensure_converter_scripts(install_dir: Path, llama_tag: str) -> None:
+    canonical = install_dir / "convert_hf_to_gguf.py"
+    if not canonical.exists():
+        # Hydrated source tree should have placed this file already.
+        # Fall back to a network fetch so the install is not blocked.
+        raw_base = f"https://raw.githubusercontent.com/ggml-org/llama.cpp/{llama_tag}"
+        source_url = f"{raw_base}/convert_hf_to_gguf.py"
+        data = download_bytes(
+            source_url,
+            progress_label = f"Downloading {download_label_from_url(source_url)}",
+        )
+        if not data:
+            raise RuntimeError(f"downloaded empty converter script from {source_url}")
+        if b"import " not in data and b"def " not in data and b"#!/" not in data:
+            raise RuntimeError(
+                f"downloaded converter script did not look like Python source: {source_url}"
+            )
+        atomic_write_bytes(canonical, data)
+    legacy = install_dir / "convert-hf-to-gguf.py"
+    if legacy.exists() or legacy.is_symlink():
+        legacy.unlink()
+    try:
+        legacy.symlink_to("convert_hf_to_gguf.py")
+    except OSError:
+        shutil.copy2(canonical, legacy)
+
+
+def extracted_archive_root(extract_dir: Path) -> Path:
+    children = [path for path in extract_dir.iterdir()]
+    if len(children) == 1 and children[0].is_dir():
+        return children[0]
+    return extract_dir
+
+
+def copy_directory_contents(source_dir: Path, destination: Path) -> None:
+    destination.mkdir(parents = True, exist_ok = True)
+    for item in source_dir.iterdir():
+        target = destination / item.name
+        if item.is_dir():
+            shutil.copytree(item, target, dirs_exist_ok = True)
+        else:
+            shutil.copy2(item, target)
+
+
+def hydrate_source_tree(
+    upstream_tag: str,
+    install_dir: Path,
+    work_dir: Path,
+    *,
+    expected_sha256: str,
+) -> None:
+    archive_path = work_dir / f"llama.cpp-source-{upstream_tag}.tar.gz"
+    source_urls = upstream_source_archive_urls(upstream_tag)
+    extract_dir = Path(tempfile.mkdtemp(prefix = "source-extract-", dir = work_dir))
+
+    try:
+        log(f"downloading llama.cpp source tree for upstream tag {upstream_tag}")
+        last_exc: Exception | None = None
+        downloaded = False
+        for index, source_url in enumerate(source_urls):
+            try:
+                if index > 0:
+                    log(
+                        f"retrying source tree download from fallback URL: {source_url}"
+                    )
+                download_file_verified(
+                    source_url,
+                    archive_path,
+                    expected_sha256 = expected_sha256,
+                    label = f"llama.cpp source tree for {upstream_tag}",
+                )
+                downloaded = True
+                break
+            except Exception as exc:
+                last_exc = exc
+                if index == len(source_urls) - 1:
+                    raise
+                log(f"source tree download failed from {source_url}: {exc}")
+        if not downloaded:
+            assert last_exc is not None
+            raise last_exc
+        extract_archive(archive_path, extract_dir)
+        source_root = extracted_archive_root(extract_dir)
+        required_paths = [
+            source_root / "CMakeLists.txt",
+            source_root / "convert_hf_to_gguf.py",
+            source_root / "gguf-py",
+        ]
+        missing = [
+            str(path.relative_to(source_root))
+            for path in required_paths
+            if not path.exists()
+        ]
+        if missing:
+            raise PrebuiltFallback(
+                "upstream source archive was missing required repo files: "
+                + ", ".join(missing)
+            )
+        copy_directory_contents(source_root, install_dir)
+    except PrebuiltFallback:
+        raise
+    except Exception as exc:
+        raise PrebuiltFallback(
+            f"failed to hydrate upstream llama.cpp source tree for {upstream_tag}: {exc}"
+        ) from exc
+    finally:
+        remove_tree(extract_dir)
+
+
+def normalize_install_layout(install_dir: Path, host: HostInfo) -> tuple[Path, Path]:
+    build_bin = install_dir / "build" / "bin"
+    if host.is_windows:
+        exec_dir = build_bin / "Release"
+        exec_dir.mkdir(parents = True, exist_ok = True)
+        return exec_dir / "llama-server.exe", exec_dir / "llama-quantize.exe"
+
+    install_dir.mkdir(parents = True, exist_ok = True)
+    build_bin.mkdir(parents = True, exist_ok = True)
+    return install_dir / "llama-server", install_dir / "llama-quantize"
+
+
+def discover_installed_executable(install_dir: Path, executable_name: str) -> Path:
+    direct = install_dir / executable_name
+    if direct.exists() and direct.is_file():
+        return direct
+    candidate = next(
+        (path for path in install_dir.rglob(executable_name) if path.is_file()), None
+    )
+    if candidate is None:
+        raise PrebuiltFallback(f"{executable_name} was not installed")
+    return candidate
+
+
+def write_exec_wrapper(entrypoint: Path, target: Path) -> None:
+    relative_target = os.path.relpath(target, entrypoint.parent)
+    script = "\n".join(
+        [
+            "#!/bin/sh",
+            f'exec "$(dirname "$0")/{relative_target}" "$@"',
+            "",
+        ]
+    )
+    atomic_write_bytes(entrypoint, script.encode("utf-8"))
+    os.chmod(entrypoint, 0o755)
+
+
+def create_exec_entrypoint(entrypoint: Path, target: Path) -> None:
+    if entrypoint == target:
+        return
+    if entrypoint.exists() or entrypoint.is_symlink():
+        entrypoint.unlink()
+    try:
+        entrypoint.symlink_to(os.path.relpath(target, entrypoint.parent))
+    except Exception:
+        write_exec_wrapper(entrypoint, target)
+
+
+def overlay_directory_for_choice(
+    install_dir: Path, choice: AssetChoice, host: HostInfo
+) -> Path:
+    if host.is_windows or choice.install_kind.startswith("windows"):
+        path = install_dir / "build" / "bin" / "Release"
+    else:
+        path = install_dir / "build" / "bin"
+    path.mkdir(parents = True, exist_ok = True)
+    return path
+
+
+def runtime_patterns_for_choice(choice: AssetChoice) -> list[str]:
+    if choice.install_kind in {"linux-cpu", "linux-cuda"}:
+        return [
+            "llama-server",
+            "llama-quantize",
+            "libllama.so*",
+            "libggml.so*",
+            "libggml-base.so*",
+            "libmtmd.so*",
+            "libggml-cpu-*.so*",
+            "libggml-cuda.so*",
+            "libggml-rpc.so*",
+        ]
+    if choice.install_kind in {"macos-arm64", "macos-x64"}:
+        return ["llama-server", "llama-quantize", "lib*.dylib"]
+    if choice.install_kind in {"windows-cpu", "windows-cuda"}:
+        return ["*.exe", "*.dll"]
+    raise PrebuiltFallback(
+        f"unsupported install kind for runtime overlay: {choice.install_kind}"
+    )
+
+
+def metadata_patterns_for_choice(choice: AssetChoice) -> list[str]:
+    patterns = ["BUILD_INFO.txt", "THIRD_PARTY_LICENSES.txt"]
+    if choice.install_kind.startswith("windows"):
+        patterns.append("LICENSE.txt")
+    else:
+        patterns.append("LICENSE")
+    return patterns
+
+
+@contextmanager
+def install_lock(lock_path: Path) -> Iterator[None]:
+    lock_path.parent.mkdir(parents = True, exist_ok = True)
+
+    if FileLock is None:
+        # Fallback: exclusive file creation as a simple lock.
+        # Write our PID so stale locks from crashed processes can be detected.
+        fd: int | None = None
+        deadline = time.monotonic() + INSTALL_LOCK_TIMEOUT_SECONDS
+        while True:
+            try:
+                fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_RDWR)
+                os.write(fd, f"{os.getpid()}\n".encode())
+                os.fsync(fd)
+                break
+            except FileExistsError:
+                # Check if the holder process is still alive
+                stale = False
+                try:
+                    raw = lock_path.read_text().strip()
+                except FileNotFoundError:
+                    # Lock vanished between our open attempt and read -- retry
+                    continue
+                if not raw:
+                    # File exists but PID not yet written -- another process
+                    # just created it. Wait briefly for the write to land.
+                    time.sleep(0.1)
+                    continue
+                try:
+                    holder_pid = int(raw)
+                    os.kill(holder_pid, 0)  # signal 0 = existence check
+                except ValueError:
+                    # PID unreadable (corrupted file)
+                    stale = True
+                except ProcessLookupError:
+                    # Process is dead
+                    stale = True
+                except PermissionError:
+                    # Process is alive but owned by another user -- not stale
+                    pass
+                if stale:
+                    lock_path.unlink(missing_ok = True)
+                    continue
+                if time.monotonic() >= deadline:
+                    raise RuntimeError(
+                        f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}"
+                    )
+                time.sleep(0.5)
+        try:
+            yield
+        finally:
+            if fd is not None:
+                os.close(fd)
+            lock_path.unlink(missing_ok = True)
+        return
+
+    try:
+        with FileLock(lock_path, timeout = INSTALL_LOCK_TIMEOUT_SECONDS):
+            yield
+    except FileLockTimeout as exc:
+        raise RuntimeError(
+            f"timed out after {INSTALL_LOCK_TIMEOUT_SECONDS}s waiting for concurrent install lock: {lock_path}"
+        ) from exc
+
+
+def install_lock_path(install_dir: Path) -> Path:
+    return install_dir.parent / f".{install_dir.name}.install.lock"
+
+
+def install_staging_root(install_dir: Path) -> Path:
+    root = install_dir.parent / INSTALL_STAGING_ROOT_NAME
+    root.mkdir(parents = True, exist_ok = True)
+    return root
+
+
+def prune_install_staging_root(install_dir: Path) -> None:
+    root = install_dir.parent / INSTALL_STAGING_ROOT_NAME
+    try:
+        root.rmdir()
+    except OSError:
+        pass
+
+
+def create_install_staging_dir(install_dir: Path) -> Path:
+    staging_dir = Path(
+        tempfile.mkdtemp(
+            prefix = f"{install_dir.name}.staging-", dir = install_staging_root(install_dir)
+        )
+    )
+    log(f"created install staging dir {staging_dir}")
+    return staging_dir
+
+
+def unique_install_side_path(install_dir: Path, label: str) -> Path:
+    root = install_staging_root(install_dir)
+    timestamp = time.strftime("%Y%m%d%H%M%S", time.gmtime())
+    prefix = f"{install_dir.name}.{label}-{timestamp}-{os.getpid()}"
+    candidate = root / prefix
+    counter = 0
+    while candidate.exists():
+        counter += 1
+        candidate = root / f"{prefix}-{counter}"
+    return candidate
+
+
+def remove_tree(path: Path | None) -> None:
+    if path and path.exists():
+        shutil.rmtree(path, ignore_errors = True)
+
+
+def remove_tree_logged(path: Path | None, label: str) -> None:
+    if not path:
+        return
+    if not path.exists():
+        log(f"{label} already absent at {path}")
+        return
+    log(f"removing {label} at {path}")
+    try:
+        shutil.rmtree(path)
+    except Exception as exc:
+        log(f"failed to remove {label} at {path}: {exc}")
+        raise
+
+
+def cleanup_install_side_paths(
+    install_dir: Path,
+    *,
+    staging_dir: Path | None = None,
+    rollback_dir: Path | None = None,
+    failed_dir: Path | None = None,
+    active_dir: Path | None = None,
+) -> None:
+    cleanup_failures: list[str] = []
+    for label, path in (
+        ("failed install path", failed_dir),
+        ("rollback path", rollback_dir),
+        ("active install path", active_dir),
+        ("staging dir", staging_dir),
+    ):
+        if not path:
+            continue
+        try:
+            remove_tree_logged(path, label)
+        except Exception as exc:
+            cleanup_failures.append(f"{label} ({path}): {exc}")
+    prune_install_staging_root(install_dir)
+    if cleanup_failures:
+        raise RuntimeError("cleanup failed for " + "; ".join(cleanup_failures))
+
+
+def confirm_install_tree(install_dir: Path, host: HostInfo) -> None:
+    if host.is_windows:
+        expected = [
+            install_dir / "build" / "bin" / "Release" / "llama-server.exe",
+            install_dir / "build" / "bin" / "Release" / "llama-quantize.exe",
+            install_dir / "convert_hf_to_gguf.py",
+            install_dir / "gguf-py",
+        ]
+    else:
+        expected = [
+            install_dir / "llama-server",
+            install_dir / "llama-quantize",
+            install_dir / "build" / "bin" / "llama-server",
+            install_dir / "build" / "bin" / "llama-quantize",
+            install_dir / "convert_hf_to_gguf.py",
+            install_dir / "gguf-py",
+        ]
+
+    expected.append(install_dir / "UNSLOTH_PREBUILT_INFO.json")
+    missing = [str(path) for path in expected if not path.exists()]
+    if missing:
+        raise RuntimeError(
+            "activated install was missing expected files: " + ", ".join(missing)
+        )
+
+
+def activate_install_tree(staging_dir: Path, install_dir: Path, host: HostInfo) -> None:
+    rollback_dir: Path | None = None
+    failed_dir: Path | None = None
+    try:
+        if install_dir.exists():
+            rollback_dir = unique_install_side_path(install_dir, "rollback")
+            log(f"moving existing install to rollback path {rollback_dir}")
+            os.replace(install_dir, rollback_dir)
+            log(f"moved existing install to rollback path {rollback_dir.name}")
+
+        log(f"activating staged install {staging_dir} -> {install_dir}")
+        os.replace(staging_dir, install_dir)
+        log(f"activated staged install at {install_dir}")
+        log(f"confirming activated install tree at {install_dir}")
+        confirm_install_tree(install_dir, host)
+        log(f"activated install tree confirmed at {install_dir}")
+    except Exception as exc:
+        log(f"activation failed for staged install: {exc}")
+        try:
+            if install_dir.exists():
+                failed_dir = unique_install_side_path(install_dir, "failed")
+                log(f"moving failed active install to {failed_dir}")
+                os.replace(install_dir, failed_dir)
+            elif staging_dir.exists():
+                failed_dir = staging_dir
+                staging_dir = None
+                log(f"retaining failed staging tree at {failed_dir}")
+
+            if rollback_dir and rollback_dir.exists():
+                log(f"restoring rollback path {rollback_dir} -> {install_dir}")
+                os.replace(rollback_dir, install_dir)
+                log(f"restored previous install from rollback path {rollback_dir.name}")
+                raise PrebuiltFallback(
+                    "staged prebuilt validation passed but activation failed; restored previous install "
+                    f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})"
+                ) from exc
+        except PrebuiltFallback:
+            raise
+        except Exception as rollback_exc:
+            log(f"rollback after failed activation also failed: {rollback_exc}")
+
+        log(
+            "rollback restoration failed; cleaning staging, install, and rollback paths before source build fallback"
+        )
+        cleanup_error: Exception | None = None
+        try:
+            cleanup_install_side_paths(
+                install_dir,
+                staging_dir = staging_dir,
+                rollback_dir = rollback_dir,
+                failed_dir = failed_dir,
+                active_dir = install_dir,
+            )
+        except Exception as cleanup_exc:
+            cleanup_error = cleanup_exc
+            log(f"cleanup after rollback failure also failed: {cleanup_exc}")
+        details = textwrap.shorten(str(exc), width = 200, placeholder = "...")
+        if cleanup_error is not None:
+            raise PrebuiltFallback(
+                "staged prebuilt validation passed but activation and rollback failed; "
+                f"cleanup also reported errors ({details}; cleanup={cleanup_error})"
+            ) from exc
+        raise PrebuiltFallback(
+            "staged prebuilt validation passed but activation and rollback failed; "
+            f"cleaned install state for fresh source build ({details})"
+        ) from exc
+    else:
+        if rollback_dir:
+            remove_tree_logged(rollback_dir, "rollback path")
+    finally:
+        remove_tree(failed_dir)
+        remove_tree(staging_dir)
+        prune_install_staging_root(install_dir)
+
+
+def install_from_archives(
+    choice: AssetChoice, host: HostInfo, install_dir: Path, work_dir: Path
+) -> tuple[Path, Path]:
+    main_archive = work_dir / choice.name
+    log(f"downloading {choice.name} from {choice.source_label} release")
+    if not choice.expected_sha256:
+        raise PrebuiltFallback(
+            f"approved checksum was missing for selected asset {choice.name}"
+        )
+    download_file_verified(
+        choice.url,
+        main_archive,
+        expected_sha256 = choice.expected_sha256,
+        label = f"prebuilt archive {choice.name}",
+    )
+
+    install_dir.mkdir(parents = True, exist_ok = True)
+    extract_dir = Path(tempfile.mkdtemp(prefix = "extract-", dir = work_dir))
+
+    try:
+        extract_archive(main_archive, extract_dir)
+        source_dir = extract_dir
+        overlay_dir = overlay_directory_for_choice(install_dir, choice, host)
+        copy_globs(
+            source_dir, overlay_dir, runtime_patterns_for_choice(choice), required = True
+        )
+        copy_globs(
+            source_dir,
+            install_dir,
+            metadata_patterns_for_choice(choice),
+            required = False,
+        )
+    finally:
+        remove_tree(extract_dir)
+
+    if host.is_windows:
+        exec_dir = install_dir / "build" / "bin" / "Release"
+        server_src = next(exec_dir.glob("llama-server.exe"), None)
+        quantize_src = next(exec_dir.glob("llama-quantize.exe"), None)
+        if server_src is None or quantize_src is None:
+            raise PrebuiltFallback("windows executables were not installed correctly")
+        return server_src, quantize_src
+
+    build_bin = install_dir / "build" / "bin"
+    source_server = build_bin / "llama-server"
+    source_quantize = build_bin / "llama-quantize"
+    if not source_server.exists() or not source_quantize.exists():
+        raise PrebuiltFallback(
+            "unix executables were not installed correctly into build/bin"
+        )
+    os.chmod(source_server, 0o755)
+    os.chmod(source_quantize, 0o755)
+
+    root_server = install_dir / "llama-server"
+    root_quantize = install_dir / "llama-quantize"
+    if source_server != root_server:
+        create_exec_entrypoint(root_server, source_server)
+    if source_quantize != root_quantize:
+        create_exec_entrypoint(root_quantize, source_quantize)
+    build_server = build_bin / "llama-server"
+    build_quantize = build_bin / "llama-quantize"
+    if source_server != build_server:
+        create_exec_entrypoint(build_server, source_server)
+    if source_quantize != build_quantize:
+        create_exec_entrypoint(build_quantize, source_quantize)
+
+    return source_server, source_quantize
+
+
+def ensure_repo_shape(install_dir: Path) -> None:
+    required = [
+        install_dir / "CMakeLists.txt",
+        install_dir / "convert_hf_to_gguf.py",
+        install_dir / "gguf-py",
+    ]
+    missing = [
+        str(path.relative_to(install_dir)) for path in required if not path.exists()
+    ]
+    if missing:
+        raise PrebuiltFallback(
+            "hydrated llama.cpp source tree was missing: " + ", ".join(missing)
+        )
+
+
+def validation_model_cache_path(install_dir: Path) -> Path:
+    cache_dir = install_dir.parent / VALIDATION_MODEL_CACHE_DIRNAME
+    cache_dir.mkdir(parents = True, exist_ok = True)
+    return cache_dir / VALIDATION_MODEL_CACHE_FILENAME
+
+
+def validated_validation_model_bytes(data: bytes) -> bytes:
+    if not data:
+        raise RuntimeError(f"downloaded empty validation model from {TEST_MODEL_URL}")
+    digest = hashlib.sha256(data).hexdigest()
+    if digest != TEST_MODEL_SHA256:
+        raise RuntimeError(
+            "validation model checksum mismatch: "
+            f"expected={TEST_MODEL_SHA256} actual={digest}"
+        )
+    return data
+
+
+def download_validation_model(path: Path, cache_path: Path | None = None) -> None:
+    try:
+        data: bytes | None = None
+        if cache_path and cache_path.exists():
+            try:
+                data = validated_validation_model_bytes(cache_path.read_bytes())
+                log(f"using cached tiny GGUF validation model from {cache_path}")
+            except Exception as exc:
+                log(
+                    f"cached tiny GGUF validation model was invalid; refreshing cache ({exc})"
+                )
+                data = None
+        if data is None:
+            log("downloading tiny GGUF validation model")
+            data = validated_validation_model_bytes(
+                download_bytes(
+                    TEST_MODEL_URL,
+                    progress_label = f"Downloading {download_label_from_url(TEST_MODEL_URL)}",
+                )
+            )
+            if cache_path is not None:
+                atomic_write_bytes(cache_path, data)
+        atomic_write_bytes(path, data)
+    except Exception as exc:
+        raise PrebuiltFallback(f"validation model unavailable: {exc}") from exc
+
+
+def free_local_port() -> int:
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.bind(("127.0.0.1", 0))
+    _, port = sock.getsockname()
+    sock.close()
+    return int(port)
+
+
+def read_log_excerpt(log_path: Path, *, max_lines: int = 60) -> str:
+    try:
+        content = log_path.read_text(encoding = "utf-8", errors = "replace")
+    except FileNotFoundError:
+        return ""
+    return "\n".join(content.splitlines()[-max_lines:])
+
+
+def is_retryable_server_bind_error(
+    exc: Exception | None,
+    output: str = "",
+    *,
+    exited_quickly: bool = False,
+) -> bool:
+    haystack = output.lower()
+    bind_markers = (
+        "address already in use",
+        "only one usage of each socket address",
+        "failed to bind",
+        "bind failed",
+        "failed to listen",
+        "errno 98",
+        "errno 10048",
+    )
+    if any(marker in haystack for marker in bind_markers):
+        return True
+
+    if isinstance(exc, urllib.error.URLError):
+        reason = exc.reason
+        if exited_quickly and isinstance(reason, ConnectionRefusedError):
+            return True
+        if isinstance(reason, OSError) and reason.errno in {
+            98,
+            99,
+            111,
+            10048,
+            10049,
+            10061,
+        }:
+            return exited_quickly
+    if exited_quickly and isinstance(exc, ConnectionRefusedError):
+        return True
+    if isinstance(exc, OSError) and exc.errno in {98, 99, 111, 10048, 10049, 10061}:
+        return exited_quickly
+    return False
+
+
+def dedupe_existing_dirs(paths: Iterable[str | Path]) -> list[str]:
+    unique: list[str] = []
+    seen: set[str] = set()
+    for raw in paths:
+        if not raw:
+            continue
+        path = Path(raw).expanduser()
+        if not path.is_dir():
+            continue
+        resolved = str(path.resolve())
+        if resolved in seen:
+            continue
+        seen.add(resolved)
+        unique.append(resolved)
+    return unique
+
+
+def linux_missing_libraries(
+    binary_path: Path, *, env: dict[str, str] | None = None
+) -> list[str]:
+    try:
+        result = run_capture(["ldd", str(binary_path)], timeout = 20, env = env)
+    except Exception:
+        return []
+
+    missing: list[str] = []
+    for line in (result.stdout + result.stderr).splitlines():
+        line = line.strip()
+        if "=> not found" not in line:
+            continue
+        library = line.split("=>", 1)[0].strip()
+        if library and library not in missing:
+            missing.append(library)
+    return missing
+
+
+def python_runtime_dirs() -> list[str]:
+    candidates: list[Path] = []
+    search_roots = [Path(entry) for entry in sys.path if entry]
+    try:
+        search_roots.extend(Path(path) for path in site.getsitepackages())
+    except Exception:
+        pass
+    try:
+        user_site = site.getusersitepackages()
+        if user_site:
+            search_roots.append(Path(user_site))
+    except Exception:
+        pass
+
+    for root in search_roots:
+        if not root.is_dir():
+            continue
+        candidates.extend(root.glob("nvidia/*/lib"))
+        candidates.extend(root.glob("nvidia/*/bin"))
+        candidates.extend(root.glob("torch/lib"))
+    return dedupe_existing_dirs(candidates)
+
+
+def ldconfig_runtime_dirs(required_libraries: Iterable[str]) -> list[str]:
+    try:
+        result = run_capture(["ldconfig", "-p"], timeout = 20)
+    except Exception:
+        return []
+
+    required = set(required_libraries)
+    candidates: list[str] = []
+    for line in result.stdout.splitlines():
+        if "=>" not in line:
+            continue
+        library, _, location = line.partition("=>")
+        library = library.strip().split()[0]
+        if required and library not in required:
+            continue
+        path = Path(location.strip()).parent
+        candidates.append(str(path))
+    return dedupe_existing_dirs(candidates)
+
+
+def linux_runtime_dirs(binary_path: Path) -> list[str]:
+    missing = linux_missing_libraries(binary_path)
+    if not missing:
+        return []
+    return linux_runtime_dirs_for_required_libraries(missing)
+
+
+def preflight_linux_installed_binaries(
+    binaries: Iterable[Path],
+    install_dir: Path,
+    host: HostInfo,
+) -> None:
+    if not host.is_linux:
+        return
+
+    issues: list[str] = []
+    for binary_path in binaries:
+        env = binary_env(binary_path, install_dir, host)
+        missing = linux_missing_libraries(binary_path, env = env)
+        if not missing:
+            continue
+        runtime_dirs = [
+            part for part in env.get("LD_LIBRARY_PATH", "").split(os.pathsep) if part
+        ]
+        issues.append(
+            f"{binary_path.name}: missing={','.join(missing)} "
+            f"ld_library_path={','.join(runtime_dirs) if runtime_dirs else 'none'}"
+        )
+
+    if issues:
+        raise PrebuiltFallback(
+            "linux extracted binary preflight failed:\n" + "\n".join(issues)
+        )
+
+
+def glob_paths(*patterns: str) -> list[str]:
+    matches: list[str] = []
+    for pattern in patterns:
+        if any(char in pattern for char in "*?[]"):
+            matches.extend(str(path) for path in Path("/").glob(pattern.lstrip("/")))
+        else:
+            matches.append(pattern)
+    return matches
+
+
+def windows_runtime_dirs() -> list[str]:
+    candidates: list[str | Path] = []
+
+    env_dirs = os.environ.get("CUDA_RUNTIME_DLL_DIR", "")
+    if env_dirs:
+        candidates.extend(part for part in env_dirs.split(os.pathsep) if part)
+
+    path_dirs = os.environ.get("PATH", "")
+    if path_dirs:
+        candidates.extend(part for part in path_dirs.split(os.pathsep) if part)
+
+    cuda_roots: list[Path] = []
+    for name in ("CUDA_PATH", "CUDA_HOME", "CUDA_ROOT"):
+        value = os.environ.get(name)
+        if value:
+            cuda_roots.append(Path(value))
+
+    for root in cuda_roots:
+        candidates.extend([root / "bin", root / "lib" / "x64"])
+
+    program_files = os.environ.get("ProgramFiles", r"C:\Program Files")
+    toolkit_base = Path(program_files) / "NVIDIA GPU Computing Toolkit" / "CUDA"
+    if toolkit_base.is_dir():
+        candidates.extend(toolkit_base.glob("v*/bin"))
+        candidates.extend(toolkit_base.glob("v*/lib/x64"))
+
+    candidates.extend(Path(path) for path in python_runtime_dirs())
+    return dedupe_existing_dirs(candidates)
+
+
+def windows_runtime_dirs_for_patterns(
+    required_patterns: Iterable[str],
+    candidate_dirs: Iterable[str] | None = None,
+) -> list[str]:
+    directories = (
+        list(candidate_dirs) if candidate_dirs is not None else windows_runtime_dirs()
+    )
+    matching_dirs: list[str] = []
+    for pattern in required_patterns:
+        matched_dirs = [
+            directory for directory in directories if any(Path(directory).glob(pattern))
+        ]
+        if not matched_dirs:
+            return []
+        for directory in matched_dirs:
+            if directory not in matching_dirs:
+                matching_dirs.append(directory)
+    return matching_dirs
+
+
+def windows_runtime_dirs_for_runtime_line(runtime_line: str | None) -> list[str]:
+    if not runtime_line:
+        return []
+    patterns = windows_runtime_line_info().get(runtime_line)
+    if not patterns:
+        return []
+    return windows_runtime_dirs_for_patterns(patterns)
+
+
+def binary_env(
+    binary_path: Path,
+    install_dir: Path,
+    host: HostInfo,
+    *,
+    runtime_line: str | None = None,
+) -> dict[str, str]:
+    env = os.environ.copy()
+    if host.is_windows:
+        path_dirs = [
+            str(binary_path.parent),
+            *windows_runtime_dirs_for_runtime_line(runtime_line),
+        ]
+        existing = [part for part in env.get("PATH", "").split(os.pathsep) if part]
+        env["PATH"] = os.pathsep.join(dedupe_existing_dirs([*path_dirs, *existing]))
+    elif host.is_linux:
+        ld_dirs = [
+            str(binary_path.parent),
+            str(install_dir),
+            *linux_runtime_dirs(binary_path),
+        ]
+        existing = [
+            part for part in env.get("LD_LIBRARY_PATH", "").split(os.pathsep) if part
+        ]
+        env["LD_LIBRARY_PATH"] = os.pathsep.join(
+            dedupe_existing_dirs([*ld_dirs, *existing])
+        )
+    elif host.is_macos:
+        dyld_dirs = [str(binary_path.parent), str(install_dir)]
+        existing = [
+            part for part in env.get("DYLD_LIBRARY_PATH", "").split(os.pathsep) if part
+        ]
+        env["DYLD_LIBRARY_PATH"] = os.pathsep.join(
+            dedupe_existing_dirs([*dyld_dirs, *existing])
+        )
+    return env
+
+
+def validate_quantize(
+    quantize_path: Path,
+    probe_path: Path,
+    quantized_path: Path,
+    install_dir: Path,
+    host: HostInfo,
+    *,
+    runtime_line: str | None = None,
+) -> None:
+    command = [str(quantize_path), str(probe_path), str(quantized_path), "Q6_K", "2"]
+    result = subprocess.run(
+        command,
+        capture_output = True,
+        text = True,
+        timeout = 120,
+        env = binary_env(quantize_path, install_dir, host, runtime_line = runtime_line),
+    )
+    if (
+        result.returncode != 0
+        or not quantized_path.exists()
+        or quantized_path.stat().st_size == 0
+    ):
+        raise PrebuiltFallback(
+            "llama-quantize validation failed:\n"
+            + result.stdout
+            + ("\n" + result.stderr if result.stderr else "")
+        )
+
+
+def validate_server(
+    server_path: Path,
+    probe_path: Path,
+    host: HostInfo,
+    install_dir: Path,
+    *,
+    runtime_line: str | None = None,
+) -> None:
+    last_failure: PrebuiltFallback | None = None
+    for port_attempt in range(1, SERVER_PORT_BIND_ATTEMPTS + 1):
+        port = free_local_port()
+        command = [
+            str(server_path),
+            "-m",
+            str(probe_path),
+            "--host",
+            "127.0.0.1",
+            "--port",
+            str(port),
+            "-c",
+            "32",
+            "--parallel",
+            "1",
+            "--threads",
+            "1",
+            "--ubatch-size",
+            "32",
+            "--batch-size",
+            "32",
+        ]
+        if host.has_usable_nvidia or (host.is_macos and host.is_arm64):
+            command.extend(["--n-gpu-layers", "1"])
+
+        log_fd, log_name = tempfile.mkstemp(prefix = "llama-server-", suffix = ".log")
+        os.close(log_fd)
+        log_path = Path(log_name)
+        process: subprocess.Popen[str] | None = None
+        try:
+            with log_path.open("w", encoding = "utf-8", errors = "replace") as log_handle:
+                process = subprocess.Popen(
+                    command,
+                    stdout = log_handle,
+                    stderr = subprocess.STDOUT,
+                    text = True,
+                    env = binary_env(
+                        server_path, install_dir, host, runtime_line = runtime_line
+                    ),
+                )
+                deadline = time.time() + 20
+                startup_started = time.time()
+                response_body = ""
+                last_error: Exception | None = None
+                while time.time() < deadline:
+                    if process.poll() is not None:
+                        process.wait(timeout = 5)
+                        log_handle.flush()
+                        output = read_log_excerpt(log_path)
+                        exited_quickly = (
+                            time.time() - startup_started
+                        ) <= SERVER_BIND_RETRY_WINDOW_SECONDS
+                        failure = PrebuiltFallback(
+                            "llama-server exited during startup:\n" + output
+                        )
+                        if (
+                            port_attempt < SERVER_PORT_BIND_ATTEMPTS
+                            and is_retryable_server_bind_error(
+                                last_error,
+                                output,
+                                exited_quickly = exited_quickly,
+                            )
+                        ):
+                            log(
+                                f"llama-server startup hit a port race on {port}; retrying with a fresh port "
+                                f"({port_attempt}/{SERVER_PORT_BIND_ATTEMPTS})"
+                            )
+                            last_failure = failure
+                            break
+                        raise failure
+
+                    payload = json.dumps({"prompt": "a", "n_predict": 1}).encode(
+                        "utf-8"
+                    )
+                    request = urllib.request.Request(
+                        f"http://127.0.0.1:{port}/completion",
+                        data = payload,
+                        headers = {"Content-Type": "application/json"},
+                    )
+                    try:
+                        with urllib.request.urlopen(request, timeout = 5) as response:
+                            status_code = response.status
+                            response_body = response.read().decode("utf-8", "replace")
+                            if status_code == 200:
+                                return
+                            last_error = RuntimeError(
+                                f"unexpected HTTP status {status_code}"
+                            )
+                    except urllib.error.HTTPError as exc:
+                        response_body = exc.read().decode("utf-8", "replace")
+                        last_error = exc
+                    except Exception as exc:
+                        last_error = exc
+                    time.sleep(0.5)
+                else:
+                    log_handle.flush()
+                    output = read_log_excerpt(log_path)
+                    raise PrebuiltFallback(
+                        "llama-server completion validation timed out"
+                        + (f" ({last_error})" if last_error else "")
+                        + ":\n"
+                        + output
+                        + ("\n" + response_body if response_body else "")
+                    )
+        finally:
+            if process is not None and process.poll() is None:
+                process.terminate()
+                try:
+                    process.wait(timeout = 5)
+                except subprocess.TimeoutExpired:
+                    process.kill()
+                    process.wait(timeout = 5)
+            try:
+                log_path.unlink(missing_ok = True)
+            except Exception:
+                pass
+    if last_failure is not None:
+        raise last_failure
+    raise PrebuiltFallback("llama-server validation failed unexpectedly")
+
+
+def collect_system_report(
+    host: HostInfo, choice: AssetChoice | None, install_dir: Path
+) -> str:
+    lines = [
+        f"platform={host.system} machine={host.machine}",
+        f"driver_cuda_version={host.driver_cuda_version}",
+        f"compute_caps={','.join(host.compute_caps) if host.compute_caps else 'unknown'}",
+        f"cuda_visible_devices={host.visible_cuda_devices if host.visible_cuda_devices is not None else 'unset'}",
+        f"has_physical_nvidia={host.has_physical_nvidia}",
+        f"has_usable_nvidia={host.has_usable_nvidia}",
+        f"chosen_asset={(choice.name if choice else 'none')}",
+        f"asset_source={(choice.source_label if choice else 'none')}",
+    ]
+    if host.is_linux and host.has_physical_nvidia:
+        runtime_lines, runtime_dirs = detected_linux_runtime_lines()
+        lines.append(
+            "linux_runtime_lines="
+            + (",".join(runtime_lines) if runtime_lines else "none")
+        )
+        for runtime_line in ("cuda13", "cuda12"):
+            lines.append(
+                f"linux_runtime_dirs_{runtime_line}="
+                + (
+                    ",".join(runtime_dirs.get(runtime_line, []))
+                    if runtime_dirs.get(runtime_line)
+                    else "none"
+                )
+            )
+    if choice and choice.selection_log:
+        lines.append("selection_log:")
+        lines.extend(choice.selection_log)
+    if host.nvidia_smi:
+        try:
+            smi = run_capture([host.nvidia_smi], timeout = 20)
+            excerpt = "\n".join((smi.stdout + smi.stderr).splitlines()[:20])
+            lines.append("nvidia-smi:")
+            lines.append(excerpt)
+        except Exception as exc:
+            lines.append(f"nvidia-smi error: {exc}")
+
+    if host.is_linux:
+        server_binary = install_dir / "llama-server"
+        if server_binary.exists():
+            server_env = binary_env(server_binary, install_dir, host)
+            lines.append(
+                "linux_missing_libs="
+                + (
+                    ",".join(linux_missing_libraries(server_binary, env = server_env))
+                    or "none"
+                )
+            )
+            lines.append(
+                "linux_runtime_dirs="
+                + (
+                    ",".join(
+                        [
+                            part
+                            for part in server_env.get("LD_LIBRARY_PATH", "").split(
+                                os.pathsep
+                            )
+                            if part
+                        ]
+                    )
+                    or "none"
+                )
+            )
+            try:
+                ldd = run_capture(
+                    ["ldd", str(server_binary)], timeout = 20, env = server_env
+                )
+                lines.append("ldd llama-server:")
+                lines.append((ldd.stdout + ldd.stderr).strip())
+            except Exception as exc:
+                lines.append(f"ldd error: {exc}")
+    elif host.is_windows:
+        lines.append(
+            "windows_runtime_dirs=" + (",".join(windows_runtime_dirs()) or "none")
+        )
+        runtime_lines, runtime_dirs = detected_windows_runtime_lines()
+        lines.append(
+            "windows_runtime_lines="
+            + (",".join(runtime_lines) if runtime_lines else "none")
+        )
+        for runtime_line in ("cuda13", "cuda12"):
+            lines.append(
+                f"windows_runtime_dirs_{runtime_line}="
+                + (
+                    ",".join(runtime_dirs.get(runtime_line, []))
+                    if runtime_dirs.get(runtime_line)
+                    else "none"
+                )
+            )
+    elif host.is_macos:
+        server_binary = install_dir / "llama-server"
+        if server_binary.exists():
+            try:
+                otool = run_capture(["otool", "-L", str(server_binary)], timeout = 20)
+                lines.append("otool -L llama-server:")
+                lines.append((otool.stdout + otool.stderr).strip())
+            except Exception as exc:
+                lines.append(f"otool error: {exc}")
+
+    return "\n".join(lines)
+
+
+def apply_approved_hashes(
+    attempts: Iterable[AssetChoice],
+    checksums: ApprovedReleaseChecksums,
+) -> list[AssetChoice]:
+    approved_attempts: list[AssetChoice] = []
+    missing_assets: list[str] = []
+    for attempt in attempts:
+        approved = checksums.artifacts.get(attempt.name)
+        if approved is None:
+            missing_assets.append(attempt.name)
+            continue
+        attempt.expected_sha256 = approved.sha256
+        approved_attempts.append(attempt)
+    if not approved_attempts:
+        missing_text = ", ".join(missing_assets) if missing_assets else "none"
+        raise PrebuiltFallback(
+            "approved checksum asset did not contain the selected prebuilt archive(s): "
+            f"{missing_text}"
+        )
+    return approved_attempts
+
+
+def require_approved_source_hash(
+    checksums: ApprovedReleaseChecksums, llama_tag: str
+) -> ApprovedArtifactHash:
+    source_asset_name = source_archive_logical_name(llama_tag)
+    approved_source = checksums.artifacts.get(source_asset_name)
+    if approved_source is None:
+        raise PrebuiltFallback(
+            f"approved checksum asset did not contain source archive {source_asset_name}"
+        )
+    return approved_source
+
+
+def resolve_install_attempts(
+    llama_tag: str,
+    host: HostInfo,
+    published_repo: str,
+    published_release_tag: str,
+) -> tuple[str, str, list[AssetChoice], ApprovedReleaseChecksums]:
+    requested_tag = llama_tag
+    resolved_tag = resolve_requested_install_tag(llama_tag, published_release_tag)
+    checksums = load_approved_release_checksums(published_repo, resolved_tag)
+    require_approved_source_hash(checksums, resolved_tag)
+
+    if host.is_linux and host.is_x86_64 and host.has_usable_nvidia:
+        linux_cuda_selection = resolve_linux_cuda_choice(
+            host, resolved_tag, published_repo, published_release_tag
+        )
+        attempts = apply_approved_hashes(linux_cuda_selection.attempts, checksums)
+        if not attempts:
+            raise PrebuiltFallback("no compatible Linux CUDA asset was found")
+        log_lines(linux_cuda_selection.selection_log)
+        return requested_tag, resolved_tag, attempts, checksums
+
+    if host.is_windows and host.is_x86_64 and host.has_usable_nvidia:
+        upstream_assets = github_release_assets(UPSTREAM_REPO, resolved_tag)
+        attempts = apply_approved_hashes(
+            resolve_windows_cuda_choices(host, resolved_tag, upstream_assets), checksums
+        )
+        if not attempts:
+            raise PrebuiltFallback("no compatible Windows CUDA asset was found")
+        if attempts[0].selection_log:
+            log_lines(attempts[0].selection_log)
+        return requested_tag, resolved_tag, attempts, checksums
+
+    choice = resolve_asset_choice(
+        host, resolved_tag, published_repo, published_release_tag
+    )
+    approved_attempts = apply_approved_hashes([choice], checksums)
+    if choice.selection_log:
+        log_lines(choice.selection_log)
+    return requested_tag, resolved_tag, approved_attempts, checksums
+
+
+def write_prebuilt_metadata(
+    install_dir: Path,
+    *,
+    requested_tag: str,
+    llama_tag: str,
+    choice: AssetChoice,
+    prebuilt_fallback_used: bool,
+) -> None:
+    metadata = {
+        "requested_tag": requested_tag,
+        "tag": llama_tag,
+        "asset": choice.name,
+        "source": choice.source_label,
+        "bundle_profile": choice.bundle_profile,
+        "runtime_line": choice.runtime_line,
+        "coverage_class": choice.coverage_class,
+        "prebuilt_fallback_used": prebuilt_fallback_used,
+        "installed_at_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+    }
+    (install_dir / "UNSLOTH_PREBUILT_INFO.json").write_text(
+        json.dumps(metadata, indent = 2) + "\n"
+    )
+
+
+def validate_prebuilt_choice(
+    choice: AssetChoice,
+    host: HostInfo,
+    install_dir: Path,
+    work_dir: Path,
+    probe_path: Path,
+    *,
+    requested_tag: str,
+    llama_tag: str,
+    approved_checksums: ApprovedReleaseChecksums,
+    prebuilt_fallback_used: bool,
+    quantized_path: Path,
+) -> tuple[Path, Path]:
+    source_archive = approved_checksums.artifacts.get(
+        source_archive_logical_name(llama_tag)
+    )
+    if source_archive is None:
+        raise PrebuiltFallback(
+            f"approved checksum asset did not contain source archive {source_archive_logical_name(llama_tag)}"
+        )
+    log(f"hydrating upstream llama.cpp source for {llama_tag} into {install_dir}")
+    hydrate_source_tree(
+        llama_tag,
+        install_dir,
+        work_dir,
+        expected_sha256 = source_archive.sha256,
+    )
+    log(f"overlaying prebuilt bundle {choice.name} into {install_dir}")
+    server_path, quantize_path = install_from_archives(
+        choice, host, install_dir, work_dir
+    )
+    preflight_linux_installed_binaries((server_path, quantize_path), install_dir, host)
+    ensure_repo_shape(install_dir)
+    write_prebuilt_metadata(
+        install_dir,
+        requested_tag = requested_tag,
+        llama_tag = llama_tag,
+        choice = choice,
+        prebuilt_fallback_used = prebuilt_fallback_used,
+    )
+    validate_quantize(
+        quantize_path,
+        probe_path,
+        quantized_path,
+        install_dir,
+        host,
+        runtime_line = choice.runtime_line,
+    )
+    validate_server(
+        server_path,
+        probe_path,
+        host,
+        install_dir,
+        runtime_line = choice.runtime_line,
+    )
+    log(f"staged prebuilt validation succeeded for {choice.name}")
+    return server_path, quantize_path
+
+
+def validate_prebuilt_attempts(
+    attempts: Iterable[AssetChoice],
+    host: HostInfo,
+    install_dir: Path,
+    work_dir: Path,
+    probe_path: Path,
+    *,
+    requested_tag: str,
+    llama_tag: str,
+    approved_checksums: ApprovedReleaseChecksums,
+) -> tuple[AssetChoice, Path, bool]:
+    attempt_list = list(attempts)
+    if not attempt_list:
+        raise PrebuiltFallback("no prebuilt bundle attempts were available")
+
+    tried_fallback = False
+    for index, attempt in enumerate(attempt_list):
+        if index > 0:
+            tried_fallback = True
+            log(
+                "retrying CUDA prebuilt "
+                f"{attempt.name} install_kind={attempt.install_kind} "
+                f"runtime_line={attempt.runtime_line} coverage_class={attempt.coverage_class}"
+            )
+
+        staging_dir = create_install_staging_dir(install_dir)
+        quantized_path = work_dir / f"stories260K-q4-{index}.gguf"
+        if quantized_path.exists():
+            quantized_path.unlink()
+        try:
+            validate_prebuilt_choice(
+                attempt,
+                host,
+                staging_dir,
+                work_dir,
+                probe_path,
+                requested_tag = requested_tag,
+                llama_tag = llama_tag,
+                approved_checksums = approved_checksums,
+                prebuilt_fallback_used = tried_fallback,
+                quantized_path = quantized_path,
+            )
+        except Exception as exc:
+            remove_tree(staging_dir)
+            prune_install_staging_root(install_dir)
+            if isinstance(exc, PrebuiltFallback):
+                attempt_error = exc
+            else:
+                attempt_error = PrebuiltFallback(
+                    f"candidate attempt failed before activation for {attempt.name}: {exc}"
+                )
+            if index == len(attempt_list) - 1:
+                raise attempt_error from exc
+            log(
+                "selected CUDA bundle failed before activation; trying next prebuilt fallback "
+                f"({textwrap.shorten(str(attempt_error), width = 200, placeholder = '...')})"
+            )
+            continue
+
+        return attempt, staging_dir, tried_fallback
+
+    raise PrebuiltFallback("no prebuilt bundle passed validation")
+
+
+def install_prebuilt(
+    install_dir: Path, llama_tag: str, published_repo: str, published_release_tag: str
+) -> None:
+    host = detect_host()
+    choice: AssetChoice | None = None
+    try:
+        with install_lock(install_lock_path(install_dir)):
+            if install_dir.exists():
+                log(
+                    f"existing llama.cpp install detected at {install_dir}; validating staged prebuilt update before replacement"
+                )
+            else:
+                log(
+                    f"no existing llama.cpp install detected at {install_dir}; performing fresh prebuilt install"
+                )
+            requested_tag, llama_tag, attempts, approved_checksums = (
+                resolve_install_attempts(
+                    llama_tag,
+                    host,
+                    published_repo,
+                    published_release_tag,
+                )
+            )
+            choice = attempts[0]
+            log(
+                f"selected {choice.name} ({choice.source_label}) for {host.system} {host.machine}"
+            )
+            with tempfile.TemporaryDirectory(prefix = "unsloth-llama-prebuilt-") as tmp:
+                work_dir = Path(tmp)
+                probe_path = work_dir / "stories260K.gguf"
+                download_validation_model(
+                    probe_path, validation_model_cache_path(install_dir)
+                )
+                choice, selected_staging_dir, _ = validate_prebuilt_attempts(
+                    attempts,
+                    host,
+                    install_dir,
+                    work_dir,
+                    probe_path,
+                    requested_tag = requested_tag,
+                    llama_tag = llama_tag,
+                    approved_checksums = approved_checksums,
+                )
+                activate_install_tree(selected_staging_dir, install_dir, host)
+                try:
+                    ensure_converter_scripts(install_dir, llama_tag)
+                except Exception as exc:
+                    log(
+                        "converter script fetch failed after activation; install remains valid "
+                        f"({textwrap.shorten(str(exc), width = 200, placeholder = '...')})"
+                    )
+    except PrebuiltFallback as exc:
+        log("prebuilt install path failed; falling back to source build")
+        log(f"prebuilt fallback reason: {exc}")
+        report = collect_system_report(host, choice, install_dir)
+        print(report)
+        raise SystemExit(EXIT_FALLBACK) from exc
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description = "Install and validate a prebuilt llama.cpp bundle for Unsloth Studio."
+    )
+    parser.add_argument("--install-dir", help = "Target ~/.unsloth/llama.cpp directory")
+    parser.add_argument(
+        "--llama-tag",
+        default = DEFAULT_LLAMA_TAG,
+        help = f"llama.cpp release tag. Prebuilt installs are pinned to the approved tag {APPROVED_PREBUILT_LLAMA_TAG}.",
+    )
+    parser.add_argument(
+        "--published-repo",
+        default = DEFAULT_PUBLISHED_REPO,
+        help = "Published bundle repository",
+    )
+    parser.add_argument(
+        "--published-release-tag",
+        default = DEFAULT_PUBLISHED_TAG,
+        help = "Published GitHub release tag to pin. By default, scan releases until a compatible llama.cpp bundle is found.",
+    )
+    resolve_group = parser.add_mutually_exclusive_group()
+    resolve_group.add_argument(
+        "--resolve-llama-tag",
+        nargs = "?",
+        const = "latest",
+        help = "Resolve a llama.cpp tag such as 'latest' to the logical upstream release tag.",
+    )
+    resolve_group.add_argument(
+        "--resolve-install-tag",
+        nargs = "?",
+        const = "latest",
+        help = "Resolve a llama.cpp tag such as 'latest' to the concrete tag installable on the current host.",
+    )
+    return parser.parse_args()
+
+
+def main() -> int:
+    args = parse_args()
+    if args.resolve_llama_tag is not None:
+        print(resolve_requested_llama_tag(args.resolve_llama_tag))
+        return EXIT_SUCCESS
+
+    if args.resolve_install_tag is not None:
+        print(
+            resolve_requested_install_tag(
+                args.resolve_install_tag, args.published_release_tag or ""
+            )
+        )
+        return EXIT_SUCCESS
+
+    if not args.install_dir:
+        raise SystemExit(
+            "install_llama_prebuilt.py: --install-dir is required unless --resolve-llama-tag or --resolve-install-tag is used"
+        )
+    install_prebuilt(
+        install_dir = Path(args.install_dir).expanduser().resolve(),
+        llama_tag = args.llama_tag,
+        published_repo = args.published_repo,
+        published_release_tag = args.published_release_tag or "",
+    )
+    return EXIT_SUCCESS
+
+
+if __name__ == "__main__":
+    try:
+        raise SystemExit(main())
+    except SystemExit:
+        raise
+    except Exception as exc:
+        message = textwrap.shorten(str(exc), width = 400, placeholder = "...")
+        log(f"fatal helper error: {message}")
+        raise SystemExit(EXIT_ERROR)
diff --git a/studio/setup.ps1 b/studio/setup.ps1
index c58bcd5c8d..d8465fd039 100644
--- a/studio/setup.ps1
+++ b/studio/setup.ps1
@@ -503,7 +503,6 @@ if ($DriverMaxCuda) {
             $isCompat = ($tkMaj -lt $drMajorCuda) -or ($tkMaj -eq $drMajorCuda -and $tkMin -le $drMinorCuda)
             if ($isCompat) {
                 # Also verify the toolkit supports our GPU architecture
-                Write-Host "   [DEBUG] Checking CUDA compatibility: toolkit=$tkMaj.$tkMin arch=sm_$CudaArch" -ForegroundColor Magenta
                 $archOk = $true
                 if ($CudaArch) {
                     $archOk = Test-NvccArchSupport -NvccExe $candidateNvcc -Arch $CudaArch
@@ -1296,6 +1295,93 @@ if ($LASTEXITCODE -ne 0) {
 $ErrorActionPreference = $prevEAP_t5
 Write-Host "[OK] Transformers 5.x pre-installed to .venv_t5/" -ForegroundColor Green
 
+# ==========================================================================
+#  PHASE 3.4: Prefer prebuilt llama.cpp bundles before source build
+# ==========================================================================
+$UnslothHome = Join-Path $env:USERPROFILE ".unsloth"
+if (-not (Test-Path $UnslothHome)) { New-Item -ItemType Directory -Force $UnslothHome | Out-Null }
+$LlamaCppDir = Join-Path $UnslothHome "llama.cpp"
+$NeedLlamaSourceBuild = $false
+$SkipPrebuiltInstall = $false
+$RequestedLlamaTag = if ($env:UNSLOTH_LLAMA_TAG) { $env:UNSLOTH_LLAMA_TAG } else { "latest" }
+$HelperReleaseRepo = if ($env:UNSLOTH_LLAMA_RELEASE_REPO) { $env:UNSLOTH_LLAMA_RELEASE_REPO } else { "unslothai/llama.cpp" }
+$resolveOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-install-tag $RequestedLlamaTag --published-repo $HelperReleaseRepo 2>&1
+$resolveExit = $LASTEXITCODE
+$ResolvedLlamaTag = if ($resolveOutput) { ($resolveOutput | Select-Object -Last 1).ToString().Trim() } else { "" }
+if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) {
+    Write-Host ""
+    Write-Host "[WARN] Failed to resolve an installable prebuilt llama.cpp tag via $HelperReleaseRepo" -ForegroundColor Yellow
+    if ($resolveOutput) {
+        $resolveOutput | ForEach-Object { Write-Host $_ }
+    }
+    $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-llama-tag $RequestedLlamaTag 2>$null
+    $fallbackExit = $LASTEXITCODE
+    $ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) {
+        ($fallbackOutput | Select-Object -Last 1).ToString().Trim()
+    } elseif ($RequestedLlamaTag -eq "latest") {
+        # Try Unsloth release repo first, then fall back to ggml-org upstream
+        $resolvedLatest = $null
+        try {
+            $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/$HelperReleaseRepo/releases/latest" -ErrorAction Stop
+            $resolvedLatest = $latestRelease.tag_name
+        } catch {}
+        if (-not $resolvedLatest) {
+            try {
+                $latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/ggml-org/llama.cpp/releases/latest" -ErrorAction Stop
+                $resolvedLatest = $latestRelease.tag_name
+            } catch {}
+        }
+        if ($resolvedLatest) { $resolvedLatest } else { $RequestedLlamaTag }
+    } else {
+        $RequestedLlamaTag
+    }
+    $NeedLlamaSourceBuild = $true
+    $SkipPrebuiltInstall = $true
+}
+
+Write-Host ""
+Write-Host "Resolved llama.cpp release tag: $ResolvedLlamaTag" -ForegroundColor Gray
+
+if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") {
+    Write-Host ""
+    Write-Host "[WARN] UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt llama.cpp install" -ForegroundColor Yellow
+    $NeedLlamaSourceBuild = $true
+} else {
+    Write-Host ""
+    Write-Host "Installing prebuilt llama.cpp bundle (preferred path)..." -ForegroundColor Cyan
+    if (Test-Path $LlamaCppDir) {
+        Write-Host "Existing llama.cpp install detected -- validating staged prebuilt update before replacement" -ForegroundColor Gray
+    }
+    if ($SkipPrebuiltInstall) {
+        Write-Host "[WARN] Skipping prebuilt install because prebuilt tag resolution failed -- falling back to source build" -ForegroundColor Yellow
+    } else {
+        $prebuiltArgs = @(
+            "$PSScriptRoot\install_llama_prebuilt.py",
+            "--install-dir", $LlamaCppDir,
+            "--llama-tag", $ResolvedLlamaTag,
+            "--published-repo", $HelperReleaseRepo
+        )
+        if ($env:UNSLOTH_LLAMA_RELEASE_TAG) {
+            $prebuiltArgs += @("--published-release-tag", $env:UNSLOTH_LLAMA_RELEASE_TAG)
+        }
+        $prevEAPPrebuilt = $ErrorActionPreference
+        $ErrorActionPreference = "Continue"
+        & python @prebuiltArgs
+        $prebuiltExit = $LASTEXITCODE
+        $ErrorActionPreference = $prevEAPPrebuilt
+
+        if ($prebuiltExit -eq 0) {
+            Write-Host "[OK] Prebuilt llama.cpp installed and validated" -ForegroundColor Green
+        } else {
+            if (Test-Path $LlamaCppDir) {
+                Write-Host "[WARN] Prebuilt update failed; existing install was restored or cleaned before source build fallback" -ForegroundColor Yellow
+            }
+            Write-Host "[WARN] Prebuilt llama.cpp path unavailable or failed validation -- falling back to source build" -ForegroundColor Yellow
+            $NeedLlamaSourceBuild = $true
+        }
+    }
+}
+
 # ==========================================================================
 #  PHASE 3.5: Install OpenSSL dev (for HTTPS support in llama-server)
 # ==========================================================================
@@ -1303,42 +1389,46 @@ Write-Host "[OK] Transformers 5.x pre-installed to .venv_t5/" -ForegroundColor G
 # ShiningLight.OpenSSL.Dev includes headers + libs that cmake can find.
 $OpenSslAvailable = $false
 
-# Check if OpenSSL dev is already installed (look for include dir)
-$OpenSslRoots = @(
-    'C:\Program Files\OpenSSL-Win64',
-    'C:\Program Files\OpenSSL',
-    'C:\OpenSSL-Win64'
-)
-$OpenSslRoot = $null
-foreach ($root in $OpenSslRoots) {
-    if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) {
-        $OpenSslRoot = $root
-        break
-    }
-}
-
-if ($OpenSslRoot) {
-    $OpenSslAvailable = $true
-    Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green
-} else {
-    Write-Host "" 
-    Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan
-    $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue)
-    if ($HasWinget) {
-        winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements
-        # Re-check after install
-        foreach ($root in $OpenSslRoots) {
-            if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) {
-                $OpenSslRoot = $root
-                $OpenSslAvailable = $true
-                Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green
-                break
-            }
+if ($NeedLlamaSourceBuild) {
+    # Check if OpenSSL dev is already installed (look for include dir)
+    $OpenSslRoots = @(
+        'C:\Program Files\OpenSSL-Win64',
+        'C:\Program Files\OpenSSL',
+        'C:\OpenSSL-Win64'
+    )
+    $OpenSslRoot = $null
+    foreach ($root in $OpenSslRoots) {
+        if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) {
+            $OpenSslRoot = $root
+            break
         }
     }
-    if (-not $OpenSslAvailable) {
-        Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow
+
+    if ($OpenSslRoot) {
+        $OpenSslAvailable = $true
+        Write-Host "[OK] OpenSSL dev found at $OpenSslRoot" -ForegroundColor Green
+    } else {
+        Write-Host "" 
+        Write-Host "Installing OpenSSL dev (for HTTPS in llama-server)..." -ForegroundColor Cyan
+        $HasWinget = $null -ne (Get-Command winget -ErrorAction SilentlyContinue)
+        if ($HasWinget) {
+            winget install -e --id ShiningLight.OpenSSL.Dev --accept-package-agreements --accept-source-agreements
+            # Re-check after install
+            foreach ($root in $OpenSslRoots) {
+                if (Test-Path (Join-Path $root 'include\openssl\ssl.h')) {
+                    $OpenSslRoot = $root
+                    $OpenSslAvailable = $true
+                    Write-Host "[OK] OpenSSL dev installed at $OpenSslRoot" -ForegroundColor Green
+                    break
+                }
+            }
+        }
+        if (-not $OpenSslAvailable) {
+            Write-Host "[WARN] OpenSSL dev not available -- llama-server will be built without HTTPS" -ForegroundColor Yellow
+        }
     }
+} else {
+    Write-Host "[SKIP] OpenSSL dev install -- prebuilt llama.cpp already validated" -ForegroundColor Yellow
 }
 
 # ==========================================================================
@@ -1351,9 +1441,7 @@ if ($OpenSslRoot) {
 #   - llama-server:   for GGUF model inference (with HTTPS if OpenSSL available)
 #   - llama-quantize: for GGUF export quantization
 # Prerequisites (git, cmake, VS Build Tools, CUDA Toolkit) already installed in Phase 1.
-$UnslothHome = Join-Path $env:USERPROFILE ".unsloth"
-if (-not (Test-Path $UnslothHome)) { New-Item -ItemType Directory -Force $UnslothHome | Out-Null }
-$LlamaCppDir = Join-Path $UnslothHome "llama.cpp"
+$OriginalLlamaCppDir = $LlamaCppDir
 $BuildDir = Join-Path $LlamaCppDir "build"
 $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe"
 
@@ -1376,7 +1464,10 @@ if (Test-Path $LlamaServerBin) {
     }
 }
 
-if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) {
+if (-not $NeedLlamaSourceBuild) {
+    Write-Host ""
+    Write-Host "[OK] Using validated prebuilt llama.cpp install at $LlamaCppDir" -ForegroundColor Green
+} elseif ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) {
     Write-Host ""
     Write-Host "[OK] llama-server already exists at $LlamaServerBin" -ForegroundColor Green
 } elseif (-not $HasCmakeForBuild) {
@@ -1432,29 +1523,49 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) {
 
     # -- Step A: Clone or pull llama.cpp --
 
+    $UseConcreteRef = ($ResolvedLlamaTag -ne "latest" -and -not [string]::IsNullOrWhiteSpace($ResolvedLlamaTag))
+
     if (Test-Path (Join-Path $LlamaCppDir ".git")) {
-        Write-Host "   llama.cpp repo already cloned, pulling latest..." -ForegroundColor Gray
-        git -C $LlamaCppDir pull 2>&1 | Out-Null
+        Write-Host "   Syncing llama.cpp to $ResolvedLlamaTag..." -ForegroundColor Gray
+        if ($UseConcreteRef) {
+            git -C $LlamaCppDir fetch --depth 1 origin $ResolvedLlamaTag 2>&1 | Out-Null
+        } else {
+            git -C $LlamaCppDir fetch --depth 1 origin 2>&1 | Out-Null
+        }
         if ($LASTEXITCODE -ne 0) {
-            Write-Host "   [WARN] git pull failed -- using existing source" -ForegroundColor Yellow
+            Write-Host "   [WARN] git fetch failed -- using existing source" -ForegroundColor Yellow
+        } else {
+            git -C $LlamaCppDir checkout -B unsloth-llama-build FETCH_HEAD 2>&1 | Out-Null
+            if ($LASTEXITCODE -ne 0) {
+                $BuildOk = $false
+                $FailedStep = "git checkout"
+            } else {
+                git -C $LlamaCppDir clean -fdx 2>&1 | Out-Null
+            }
         }
     } else {
-        Write-Host "   Cloning llama.cpp..." -ForegroundColor Gray
-        if (Test-Path $LlamaCppDir) { Remove-Item -Recurse -Force $LlamaCppDir }
-        git clone --depth 1 https://github.com/ggml-org/llama.cpp.git $LlamaCppDir 2>&1 | Out-Null
+        Write-Host "   Cloning llama.cpp @ $ResolvedLlamaTag..." -ForegroundColor Gray
+        $buildTmp = "$LlamaCppDir.build.$PID"
+        if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp }
+        $cloneArgs = @("clone", "--depth", "1")
+        if ($UseConcreteRef) {
+            $cloneArgs += @("--branch", $ResolvedLlamaTag)
+        }
+        $cloneArgs += @("https://github.com/ggml-org/llama.cpp.git", $buildTmp)
+        git @cloneArgs 2>&1 | Out-Null
         if ($LASTEXITCODE -ne 0) {
             $BuildOk = $false
             $FailedStep = "git clone"
+            if (Test-Path $buildTmp) { Remove-Item -Recurse -Force $buildTmp }
+        }
+        # Use temp dir for build; swap into $LlamaCppDir only after build succeeds
+        if ($BuildOk) {
+            $LlamaCppDir = $buildTmp
+            $BuildDir = Join-Path $LlamaCppDir "build"
         }
     }
 
     # -- Step B: cmake configure --
-    # Clean stale CMake cache to prevent previous CUDA settings from leaking
-    # into a CPU-only rebuild (or vice versa).
-    $CmakeCacheFile = Join-Path $BuildDir "CMakeCache.txt"
-    if (Test-Path $CmakeCacheFile) {
-        Remove-Item -Recurse -Force $BuildDir
-    }
 
     if ($BuildOk) {
         Write-Host ""
@@ -1555,6 +1666,21 @@ if ((Test-Path $LlamaServerBin) -and -not $NeedRebuild) {
         }
     }
 
+    # Swap temp build dir into final location (only if we built in a temp dir)
+    if ($BuildOk -and $LlamaCppDir -ne $OriginalLlamaCppDir) {
+        if (Test-Path $OriginalLlamaCppDir) { Remove-Item -Recurse -Force $OriginalLlamaCppDir }
+        Move-Item $LlamaCppDir $OriginalLlamaCppDir
+        $LlamaCppDir = $OriginalLlamaCppDir
+        $BuildDir = Join-Path $LlamaCppDir "build"
+        $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe"
+    } elseif (-not $BuildOk -and $LlamaCppDir -ne $OriginalLlamaCppDir) {
+        # Build failed -- clean up temp dir, preserve existing install
+        if (Test-Path $LlamaCppDir) { Remove-Item -Recurse -Force $LlamaCppDir }
+        $LlamaCppDir = $OriginalLlamaCppDir
+        $BuildDir = Join-Path $LlamaCppDir "build"
+        $LlamaServerBin = Join-Path $BuildDir "bin\Release\llama-server.exe"
+    }
+
     # Restore ErrorActionPreference
     $ErrorActionPreference = $prevEAP
 
diff --git a/studio/setup.sh b/studio/setup.sh
index 0e99173755..4cfabec95e 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -341,10 +341,98 @@ else
     echo "✅ Python dependencies up to date — skipping"
 fi
 
-# ── 7. WSL: pre-install GGUF build dependencies ──
+# ── 7. Prefer prebuilt llama.cpp bundles before any source build path ──
+UNSLOTH_HOME="$HOME/.unsloth"
+mkdir -p "$UNSLOTH_HOME"
+LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
+LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
+_NEED_LLAMA_SOURCE_BUILD=false
+_LLAMA_FORCE_COMPILE="${UNSLOTH_LLAMA_FORCE_COMPILE:-0}"
+_REQUESTED_LLAMA_TAG="${UNSLOTH_LLAMA_TAG:-latest}"
+_HELPER_RELEASE_REPO="${UNSLOTH_LLAMA_RELEASE_REPO:-unslothai/llama.cpp}"
+_RESOLVE_LLAMA_LOG="$(mktemp)"
+set +e
+python "$SCRIPT_DIR/install_llama_prebuilt.py" \
+    --resolve-install-tag "$_REQUESTED_LLAMA_TAG" \
+    --published-repo "$_HELPER_RELEASE_REPO" >"$_RESOLVE_LLAMA_LOG" 2>&1
+_RESOLVE_LLAMA_STATUS=$?
+set -e
+if [ "$_RESOLVE_LLAMA_STATUS" -eq 0 ]; then
+    _RESOLVED_LLAMA_TAG="$(tail -n 1 "$_RESOLVE_LLAMA_LOG" | tr -d '\r')"
+else
+    _RESOLVED_LLAMA_TAG=""
+fi
+if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+    echo ""
+    echo "⚠️  Failed to resolve an installable prebuilt llama.cpp tag via $_HELPER_RELEASE_REPO"
+    cat "$_RESOLVE_LLAMA_LOG" >&2 || true
+    set +e
+    _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" --resolve-llama-tag "$_REQUESTED_LLAMA_TAG" 2>/dev/null)"
+    _RESOLVE_UPSTREAM_STATUS=$?
+    set -e
+    if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+        if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then
+            # Try Unsloth release repo first, then fall back to ggml-org upstream
+            _RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${_HELPER_RELEASE_REPO}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
+            if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+                _RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
+            fi
+        fi
+        if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+            _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG"
+        fi
+    fi
+    _NEED_LLAMA_SOURCE_BUILD=true
+    _SKIP_PREBUILT_INSTALL=true
+fi
+rm -f "$_RESOLVE_LLAMA_LOG"
+
+echo ""
+echo "Resolved llama.cpp release tag: $_RESOLVED_LLAMA_TAG"
+
+if [ "$_LLAMA_FORCE_COMPILE" = "1" ]; then
+    echo ""
+    echo "⚠️  UNSLOTH_LLAMA_FORCE_COMPILE=1 -- skipping prebuilt llama.cpp install"
+    _NEED_LLAMA_SOURCE_BUILD=true
+else
+    echo ""
+    echo "Installing prebuilt llama.cpp bundle (preferred path)..."
+    if [ -d "$LLAMA_CPP_DIR" ]; then
+        echo "Existing llama.cpp install detected -- validating staged prebuilt update before replacement"
+    fi
+    if [ "${_SKIP_PREBUILT_INSTALL:-false}" = true ]; then
+        echo "⚠️  Skipping prebuilt install because prebuilt tag resolution failed -- falling back to source build"
+    else
+        _PREBUILT_CMD=(
+            python "$SCRIPT_DIR/install_llama_prebuilt.py"
+            --install-dir "$LLAMA_CPP_DIR"
+            --llama-tag "$_RESOLVED_LLAMA_TAG"
+            --published-repo "$_HELPER_RELEASE_REPO"
+        )
+        if [ -n "${UNSLOTH_LLAMA_RELEASE_TAG:-}" ]; then
+            _PREBUILT_CMD+=(--published-release-tag "$UNSLOTH_LLAMA_RELEASE_TAG")
+        fi
+        set +e
+        "${_PREBUILT_CMD[@]}"
+        _PREBUILT_STATUS=$?
+        set -e
+
+        if [ "$_PREBUILT_STATUS" -eq 0 ]; then
+            echo "✅ Prebuilt llama.cpp installed and validated"
+        else
+            if [ -d "$LLAMA_CPP_DIR" ]; then
+                echo "⚠️  Prebuilt update failed; existing install was restored or cleaned before source build fallback"
+            fi
+            echo "⚠️  Prebuilt llama.cpp path unavailable or failed validation -- falling back to source build"
+            _NEED_LLAMA_SOURCE_BUILD=true
+        fi
+    fi
+fi
+
+# ── 8. WSL: pre-install GGUF build dependencies for fallback source builds ──
 # On WSL, sudo requires a password and can't be entered during GGUF export
 # (runs in a non-interactive subprocess). Install build deps here instead.
-if grep -qi microsoft /proc/version 2>/dev/null; then
+if [ "$_NEED_LLAMA_SOURCE_BUILD" = true ] && grep -qi microsoft /proc/version 2>/dev/null; then
     echo ""
     echo "⚠️  WSL detected -- installing build dependencies for GGUF export..."
     _GGUF_DEPS="pciutils build-essential cmake curl git libcurl4-openssl-dev"
@@ -402,22 +490,19 @@ if grep -qi microsoft /proc/version 2>/dev/null; then
     fi
 fi
 
-# ── 8. Build llama.cpp binaries for GGUF inference + export ──
+# ── 9. Build llama.cpp binaries for GGUF inference + export when prebuilt install fails ──
 # Builds at ~/.unsloth/llama.cpp — a single shared location under the user's
 # home directory. This is used by both the inference server and the GGUF
 # export pipeline (unsloth-zoo).
 #   - llama-server: for GGUF model inference
 #   - llama-quantize: for GGUF export quantization (symlinked to root for check_llama_cpp())
-UNSLOTH_HOME="$HOME/.unsloth"
-mkdir -p "$UNSLOTH_HOME"
-LLAMA_CPP_DIR="$UNSLOTH_HOME/llama.cpp"
-LLAMA_SERVER_BIN="$LLAMA_CPP_DIR/build/bin/llama-server"
-if [ "${_SKIP_GGUF_BUILD:-}" = true ]; then
+if [ "$_NEED_LLAMA_SOURCE_BUILD" = false ]; then
+    :
+elif [ "${_SKIP_GGUF_BUILD:-}" = true ]; then
     echo ""
     echo "Skipping llama-server build (missing dependencies)"
     echo "   Install the missing packages and re-run setup to enable GGUF inference."
 else
-rm -rf "$LLAMA_CPP_DIR"
 {
     # Check prerequisites
     if ! command -v cmake &>/dev/null; then
@@ -432,7 +517,13 @@ rm -rf "$LLAMA_CPP_DIR"
         echo "Building llama-server for GGUF inference..."
 
         BUILD_OK=true
-        run_quiet_no_exit "clone llama.cpp" git clone --depth 1 https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_DIR" || BUILD_OK=false
+        _CLONE_BRANCH_ARGS=()
+        if [ "$_RESOLVED_LLAMA_TAG" != "latest" ] && [ -n "$_RESOLVED_LLAMA_TAG" ]; then
+            _CLONE_BRANCH_ARGS=(--branch "$_RESOLVED_LLAMA_TAG")
+        fi
+        _BUILD_TMP="${LLAMA_CPP_DIR}.build.$$"
+        rm -rf "$_BUILD_TMP"
+        run_quiet_no_exit "clone llama.cpp" git clone --depth 1 "${_CLONE_BRANCH_ARGS[@]}" https://github.com/ggml-org/llama.cpp.git "$_BUILD_TMP" || BUILD_OK=false
 
         if [ "$BUILD_OK" = true ]; then
             # Skip tests/examples we don't need (faster build)
@@ -571,21 +662,29 @@ rm -rf "$LLAMA_CPP_DIR"
                 CMAKE_GENERATOR_ARGS="-G Ninja"
             fi
 
-            run_quiet_no_exit "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$LLAMA_CPP_DIR" -B "$LLAMA_CPP_DIR/build" $CMAKE_ARGS || BUILD_OK=false
+            run_quiet_no_exit "cmake llama.cpp" cmake $CMAKE_GENERATOR_ARGS -S "$_BUILD_TMP" -B "$_BUILD_TMP/build" $CMAKE_ARGS || BUILD_OK=false
         fi
 
         if [ "$BUILD_OK" = true ]; then
-            run_quiet_no_exit "build llama-server" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false
+            run_quiet_no_exit "build llama-server" cmake --build "$_BUILD_TMP/build" --config Release --target llama-server -j"$NCPU" || BUILD_OK=false
         fi
 
         # Also build llama-quantize (needed by unsloth-zoo's GGUF export pipeline)
         if [ "$BUILD_OK" = true ]; then
-            run_quiet_no_exit "build llama-quantize" cmake --build "$LLAMA_CPP_DIR/build" --config Release --target llama-quantize -j"$NCPU" || true
-            # Symlink to llama.cpp root — check_llama_cpp() looks for the binary there
+            run_quiet_no_exit "build llama-quantize" cmake --build "$_BUILD_TMP/build" --config Release --target llama-quantize -j"$NCPU" || true
+        fi
+
+        # Swap only after build succeeds -- preserves existing install on failure
+        if [ "$BUILD_OK" = true ]; then
+            rm -rf "$LLAMA_CPP_DIR"
+            mv "$_BUILD_TMP" "$LLAMA_CPP_DIR"
+            # Symlink to llama.cpp root -- check_llama_cpp() looks for the binary there
             QUANTIZE_BIN="$LLAMA_CPP_DIR/build/bin/llama-quantize"
             if [ -f "$QUANTIZE_BIN" ]; then
                 ln -sf build/bin/llama-quantize "$LLAMA_CPP_DIR/llama-quantize"
             fi
+        else
+            rm -rf "$_BUILD_TMP"
         fi
 
         if [ "$BUILD_OK" = true ]; then
diff --git a/tests/studio/install/smoke_test_llama_prebuilt.py b/tests/studio/install/smoke_test_llama_prebuilt.py
new file mode 100644
index 0000000000..994757d2e2
--- /dev/null
+++ b/tests/studio/install/smoke_test_llama_prebuilt.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import importlib.util
+import shutil
+import sys
+import tempfile
+import time
+from pathlib import Path
+
+
+PACKAGE_ROOT = Path(__file__).resolve().parents[3]
+INSTALLER_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py"
+
+
+def load_installer_module():
+    spec = importlib.util.spec_from_file_location(
+        "studio_install_llama_prebuilt", INSTALLER_PATH
+    )
+    if spec is None or spec.loader is None:
+        raise RuntimeError(f"unable to load installer module from {INSTALLER_PATH}")
+    module = importlib.util.module_from_spec(spec)
+    sys.modules[spec.name] = module
+    spec.loader.exec_module(module)
+    return module
+
+
+installer = load_installer_module()
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description = (
+            "Run a real end-to-end prebuilt llama.cpp install into an isolated temporary "
+            "directory on the current machine."
+        )
+    )
+    parser.add_argument(
+        "--llama-tag",
+        default = "latest",
+        help = "llama.cpp tag to resolve. Defaults to the approved prebuilt tag for this host.",
+    )
+    parser.add_argument(
+        "--published-repo",
+        default = installer.DEFAULT_PUBLISHED_REPO,
+        help = "Published bundle repository used for Linux CUDA selection.",
+    )
+    parser.add_argument(
+        "--published-release-tag",
+        default = installer.DEFAULT_PUBLISHED_TAG or "",
+        help = "Optional published GitHub release tag to pin.",
+    )
+    parser.add_argument(
+        "--work-dir",
+        default = "",
+        help = (
+            "Optional directory under which the smoke install temp dir will be created. "
+            "If omitted, defaults to ./.tmp/llama-prebuilt-smoke under the current directory."
+        ),
+    )
+    parser.add_argument(
+        "--keep-temp",
+        action = "store_true",
+        help = "Keep the temporary smoke install directory after success.",
+    )
+    return parser.parse_args()
+
+
+def smoke_root_base(work_dir: str) -> Path:
+    if work_dir:
+        return Path(work_dir).expanduser().resolve()
+    return (Path.cwd() / ".tmp" / "llama-prebuilt-smoke").resolve()
+
+
+def make_smoke_root(base_dir: Path) -> Path:
+    base_dir.mkdir(parents = True, exist_ok = True)
+    timestamp = time.strftime("%Y%m%d%H%M%S", time.gmtime())
+    return Path(tempfile.mkdtemp(prefix = f"run-{timestamp}-", dir = base_dir))
+
+
+def main() -> int:
+    args = parse_args()
+    host = installer.detect_host()
+    smoke_base = smoke_root_base(args.work_dir)
+    smoke_root = make_smoke_root(smoke_base)
+    install_dir = smoke_root / "install" / "llama.cpp"
+    choice = None
+
+    print(f"[smoke] host={host.system} machine={host.machine}")
+    print(f"[smoke] temp_root={smoke_root}")
+
+    try:
+        requested_tag, resolved_tag, attempts, _approved_checksums = (
+            installer.resolve_install_attempts(
+                args.llama_tag,
+                host,
+                args.published_repo,
+                args.published_release_tag,
+            )
+        )
+        choice = attempts[0]
+        print(f"[smoke] requested_tag={requested_tag}")
+        print(f"[smoke] resolved_tag={resolved_tag}")
+        print(f"[smoke] selected_asset={choice.name}")
+        print(f"[smoke] selected_source={choice.source_label}")
+        print(f"[smoke] install_dir={install_dir}")
+        installer.install_prebuilt(
+            install_dir = install_dir,
+            llama_tag = args.llama_tag,
+            published_repo = args.published_repo,
+            published_release_tag = args.published_release_tag,
+        )
+        print(f"[smoke] PASS install_dir={install_dir}")
+        print(
+            "[smoke] note=This was a real prebuilt install into an isolated temp directory."
+        )
+        return installer.EXIT_SUCCESS
+    except SystemExit as exc:
+        code = int(exc.code) if isinstance(exc.code, int) else installer.EXIT_ERROR
+        if code == installer.EXIT_FALLBACK:
+            print(f"[smoke] FALLBACK install_dir={install_dir}")
+            print(
+                "[smoke] note=Prebuilt path failed and would fall back to source build in setup."
+            )
+            print(installer.collect_system_report(host, choice, install_dir))
+        else:
+            print(f"[smoke] ERROR exit_code={code} install_dir={install_dir}")
+        return code
+    except Exception as exc:
+        print(f"[smoke] ERROR {exc}")
+        print(installer.collect_system_report(host, choice, install_dir))
+        return installer.EXIT_ERROR
+    finally:
+        if args.keep_temp:
+            print(f"[smoke] keeping_temp_root={smoke_root}")
+        elif smoke_root.exists():
+            shutil.rmtree(smoke_root, ignore_errors = True)
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/tests/studio/install/test_install_llama_prebuilt_logic.py b/tests/studio/install/test_install_llama_prebuilt_logic.py
new file mode 100644
index 0000000000..eb30ac2745
--- /dev/null
+++ b/tests/studio/install/test_install_llama_prebuilt_logic.py
@@ -0,0 +1,630 @@
+import importlib.util
+import io
+import json
+import os
+import sys
+import tarfile
+import zipfile
+from pathlib import Path
+
+import pytest
+
+
+PACKAGE_ROOT = Path(__file__).resolve().parents[3]
+MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py"
+SPEC = importlib.util.spec_from_file_location(
+    "studio_install_llama_prebuilt", MODULE_PATH
+)
+assert SPEC is not None and SPEC.loader is not None
+INSTALL_LLAMA_PREBUILT = importlib.util.module_from_spec(SPEC)
+sys.modules[SPEC.name] = INSTALL_LLAMA_PREBUILT
+SPEC.loader.exec_module(INSTALL_LLAMA_PREBUILT)
+
+PrebuiltFallback = INSTALL_LLAMA_PREBUILT.PrebuiltFallback
+extract_archive = INSTALL_LLAMA_PREBUILT.extract_archive
+binary_env = INSTALL_LLAMA_PREBUILT.binary_env
+HostInfo = INSTALL_LLAMA_PREBUILT.HostInfo
+AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice
+ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash
+ApprovedReleaseChecksums = INSTALL_LLAMA_PREBUILT.ApprovedReleaseChecksums
+hydrate_source_tree = INSTALL_LLAMA_PREBUILT.hydrate_source_tree
+validate_prebuilt_choice = INSTALL_LLAMA_PREBUILT.validate_prebuilt_choice
+activate_install_tree = INSTALL_LLAMA_PREBUILT.activate_install_tree
+create_install_staging_dir = INSTALL_LLAMA_PREBUILT.create_install_staging_dir
+sha256_file = INSTALL_LLAMA_PREBUILT.sha256_file
+source_archive_logical_name = INSTALL_LLAMA_PREBUILT.source_archive_logical_name
+
+
+def approved_checksums_for(
+    upstream_tag: str, *, source_archive: Path, bundle_archive: Path, bundle_name: str
+) -> ApprovedReleaseChecksums:
+    return ApprovedReleaseChecksums(
+        repo = "local",
+        release_tag = upstream_tag,
+        upstream_tag = upstream_tag,
+        source_commit = None,
+        artifacts = {
+            source_archive_logical_name(upstream_tag): ApprovedArtifactHash(
+                asset_name = source_archive_logical_name(upstream_tag),
+                sha256 = sha256_file(source_archive),
+                repo = "ggml-org/llama.cpp",
+                kind = "upstream-source",
+            ),
+            bundle_name: ApprovedArtifactHash(
+                asset_name = bundle_name,
+                sha256 = sha256_file(bundle_archive),
+                repo = "local",
+                kind = "local-test-bundle",
+            ),
+        },
+    )
+
+
+def test_extract_archive_allows_safe_tar_symlink_chain(tmp_path: Path):
+    archive_path = tmp_path / "bundle.tar.gz"
+    payload = b"shared-object"
+
+    with tarfile.open(archive_path, "w:gz") as archive:
+        versioned = tarfile.TarInfo("libllama.so.0.0.1")
+        versioned.size = len(payload)
+        archive.addfile(versioned, io_bytes(payload))
+
+        soname = tarfile.TarInfo("libllama.so.0")
+        soname.type = tarfile.SYMTYPE
+        soname.linkname = "libllama.so.0.0.1"
+        archive.addfile(soname)
+
+        linker_name = tarfile.TarInfo("libllama.so")
+        linker_name.type = tarfile.SYMTYPE
+        linker_name.linkname = "libllama.so.0"
+        archive.addfile(linker_name)
+
+    destination = tmp_path / "extract"
+    extract_archive(archive_path, destination)
+
+    assert (destination / "libllama.so.0.0.1").read_bytes() == payload
+    assert (destination / "libllama.so.0").is_symlink()
+    assert (destination / "libllama.so").is_symlink()
+    assert (destination / "libllama.so").resolve().read_bytes() == payload
+
+
+def test_extract_archive_allows_safe_tar_hardlink(tmp_path: Path):
+    archive_path = tmp_path / "bundle.tar.gz"
+    payload = b"quantize"
+
+    with tarfile.open(archive_path, "w:gz") as archive:
+        target = tarfile.TarInfo("llama-quantize")
+        target.size = len(payload)
+        archive.addfile(target, io_bytes(payload))
+
+        hardlink = tarfile.TarInfo("llama-quantize-copy")
+        hardlink.type = tarfile.LNKTYPE
+        hardlink.linkname = "llama-quantize"
+        archive.addfile(hardlink)
+
+    destination = tmp_path / "extract"
+    extract_archive(archive_path, destination)
+
+    assert (destination / "llama-quantize-copy").read_bytes() == payload
+    assert not (destination / "llama-quantize-copy").is_symlink()
+
+
+def test_extract_archive_rejects_absolute_tar_symlink_target(tmp_path: Path):
+    archive_path = tmp_path / "bundle.tar.gz"
+
+    with tarfile.open(archive_path, "w:gz") as archive:
+        entry = tarfile.TarInfo("libllama.so")
+        entry.type = tarfile.SYMTYPE
+        entry.linkname = "/tmp/libllama.so.0"
+        archive.addfile(entry)
+
+    with pytest.raises(PrebuiltFallback, match = "archive link used an absolute target"):
+        extract_archive(archive_path, tmp_path / "extract")
+
+
+def test_extract_archive_rejects_escaping_tar_symlink_target(tmp_path: Path):
+    archive_path = tmp_path / "bundle.tar.gz"
+
+    with tarfile.open(archive_path, "w:gz") as archive:
+        entry = tarfile.TarInfo("libllama.so")
+        entry.type = tarfile.SYMTYPE
+        entry.linkname = "../outside/libllama.so.0"
+        archive.addfile(entry)
+
+    with pytest.raises(PrebuiltFallback, match = "archive link escaped destination"):
+        extract_archive(archive_path, tmp_path / "extract")
+
+
+def test_extract_archive_rejects_unresolved_tar_symlink_target(tmp_path: Path):
+    archive_path = tmp_path / "bundle.tar.gz"
+
+    with tarfile.open(archive_path, "w:gz") as archive:
+        entry = tarfile.TarInfo("libllama.so")
+        entry.type = tarfile.SYMTYPE
+        entry.linkname = "libllama.so.0"
+        archive.addfile(entry)
+
+    with pytest.raises(PrebuiltFallback, match = "unresolved link entries"):
+        extract_archive(archive_path, tmp_path / "extract")
+
+
+def test_extract_archive_rejects_zip_symlink_entry(tmp_path: Path):
+    archive_path = tmp_path / "bundle.zip"
+
+    with zipfile.ZipFile(archive_path, "w") as archive:
+        info = zipfile.ZipInfo("libllama.so")
+        info.create_system = 3
+        info.external_attr = 0o120777 << 16
+        archive.writestr(info, "libllama.so.0")
+
+    with pytest.raises(PrebuiltFallback, match = "zip archive contained a symlink entry"):
+        extract_archive(archive_path, tmp_path / "extract")
+
+
+def test_hydrate_source_tree_extracts_upstream_archive_contents(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    upstream_tag = "b9999"
+    archive_path = tmp_path / "llama.cpp-source.tar.gz"
+    with tarfile.open(archive_path, "w:gz") as archive:
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/CMakeLists.txt",
+            b"cmake_minimum_required(VERSION 3.14)\n",
+        )
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/convert_hf_to_gguf.py",
+            b"#!/usr/bin/env python3\nimport gguf\n",
+        )
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py",
+            b"__all__ = []\n",
+        )
+
+    source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag))
+
+    def fake_download_file(url: str, destination: Path) -> None:
+        assert url in source_urls
+        destination.write_bytes(archive_path.read_bytes())
+
+    monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "download_file", fake_download_file)
+
+    install_dir = tmp_path / "install"
+    work_dir = tmp_path / "work"
+    work_dir.mkdir()
+    hydrate_source_tree(
+        upstream_tag, install_dir, work_dir, expected_sha256 = sha256_file(archive_path)
+    )
+
+    assert (install_dir / "CMakeLists.txt").exists()
+    assert (install_dir / "convert_hf_to_gguf.py").exists()
+    assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists()
+    assert not (install_dir / f"llama.cpp-{upstream_tag}").exists()
+
+
+def test_validate_prebuilt_choice_creates_repo_shaped_linux_install(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    upstream_tag = "b9998"
+    bundle_name = "app-b9998-linux-x64-cuda13-newer.tar.gz"
+    source_archive = tmp_path / "source.tar.gz"
+    bundle_archive = tmp_path / "bundle.tar.gz"
+    with tarfile.open(source_archive, "w:gz") as archive:
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/CMakeLists.txt",
+            b"cmake_minimum_required(VERSION 3.14)\n",
+        )
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/convert_hf_to_gguf.py",
+            b"#!/usr/bin/env python3\nimport gguf\n",
+        )
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py",
+            b"__all__ = []\n",
+        )
+    with tarfile.open(bundle_archive, "w:gz") as archive:
+        add_bytes_to_tar(archive, "llama-server", b"#!/bin/sh\nexit 0\n", mode = 0o755)
+        add_bytes_to_tar(archive, "llama-quantize", b"#!/bin/sh\nexit 0\n", mode = 0o755)
+        add_bytes_to_tar(archive, "libllama.so.0.0.1", b"libllama")
+        add_symlink_to_tar(archive, "libllama.so.0", "libllama.so.0.0.1")
+        add_symlink_to_tar(archive, "libllama.so", "libllama.so.0")
+        add_bytes_to_tar(archive, "libggml.so.0.9.8", b"libggml")
+        add_symlink_to_tar(archive, "libggml.so.0", "libggml.so.0.9.8")
+        add_symlink_to_tar(archive, "libggml.so", "libggml.so.0")
+        add_bytes_to_tar(archive, "libggml-base.so.0.9.8", b"libggml-base")
+        add_symlink_to_tar(archive, "libggml-base.so.0", "libggml-base.so.0.9.8")
+        add_symlink_to_tar(archive, "libggml-base.so", "libggml-base.so.0")
+        add_bytes_to_tar(archive, "libggml-cpu-x64.so.0.9.8", b"libggml-cpu")
+        add_symlink_to_tar(archive, "libggml-cpu-x64.so.0", "libggml-cpu-x64.so.0.9.8")
+        add_symlink_to_tar(archive, "libggml-cpu-x64.so", "libggml-cpu-x64.so.0")
+        add_bytes_to_tar(archive, "libmtmd.so.0.0.1", b"libmtmd")
+        add_symlink_to_tar(archive, "libmtmd.so.0", "libmtmd.so.0.0.1")
+        add_symlink_to_tar(archive, "libmtmd.so", "libmtmd.so.0")
+        add_bytes_to_tar(archive, "BUILD_INFO.txt", b"bundle metadata\n")
+        add_bytes_to_tar(archive, "THIRD_PARTY_LICENSES.txt", b"licenses\n")
+
+    source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag))
+
+    def fake_download_file(url: str, destination: Path) -> None:
+        if url in source_urls:
+            destination.write_bytes(source_archive.read_bytes())
+            return
+        if url == "file://bundle":
+            destination.write_bytes(bundle_archive.read_bytes())
+            return
+        raise AssertionError(f"unexpected download url: {url}")
+
+    monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "download_file", fake_download_file)
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "download_bytes",
+        lambda url, **_: b"#!/usr/bin/env python3\nimport gguf\n",
+    )
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "preflight_linux_installed_binaries",
+        lambda *args, **kwargs: None,
+    )
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT, "validate_quantize", lambda *args, **kwargs: None
+    )
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT, "validate_server", lambda *args, **kwargs: None
+    )
+
+    host = HostInfo(
+        system = "Linux",
+        machine = "x86_64",
+        is_windows = False,
+        is_linux = True,
+        is_macos = False,
+        is_x86_64 = True,
+        is_arm64 = False,
+        nvidia_smi = None,
+        driver_cuda_version = None,
+        compute_caps = [],
+        visible_cuda_devices = None,
+        has_physical_nvidia = False,
+        has_usable_nvidia = False,
+    )
+    choice = AssetChoice(
+        repo = "local",
+        tag = upstream_tag,
+        name = bundle_name,
+        url = "file://bundle",
+        source_label = "local",
+        is_ready_bundle = True,
+        install_kind = "linux-cuda",
+        bundle_profile = "cuda13-newer",
+        runtime_line = "cuda13",
+        expected_sha256 = sha256_file(bundle_archive),
+    )
+
+    install_dir = tmp_path / "install"
+    work_dir = tmp_path / "work"
+    work_dir.mkdir()
+    probe_path = tmp_path / "stories260K.gguf"
+    quantized_path = tmp_path / "stories260K-q4.gguf"
+    validate_prebuilt_choice(
+        choice,
+        host,
+        install_dir,
+        work_dir,
+        probe_path,
+        requested_tag = upstream_tag,
+        llama_tag = upstream_tag,
+        approved_checksums = approved_checksums_for(
+            upstream_tag,
+            source_archive = source_archive,
+            bundle_archive = bundle_archive,
+            bundle_name = bundle_name,
+        ),
+        prebuilt_fallback_used = False,
+        quantized_path = quantized_path,
+    )
+
+    assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists()
+    assert (install_dir / "convert_hf_to_gguf.py").exists()
+    assert (install_dir / "build" / "bin" / "llama-server").exists()
+    assert (install_dir / "build" / "bin" / "llama-quantize").exists()
+    assert (install_dir / "build" / "bin" / "libllama.so").exists()
+    assert (install_dir / "llama-server").exists()
+    assert (install_dir / "llama-quantize").exists()
+    assert (install_dir / "UNSLOTH_PREBUILT_INFO.json").exists()
+    assert (install_dir / "BUILD_INFO.txt").exists()
+
+
+def test_validate_prebuilt_choice_creates_repo_shaped_windows_install(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    upstream_tag = "b9997"
+    bundle_name = "app-b9997-windows-x64-cpu.zip"
+    source_archive = tmp_path / "source.tar.gz"
+    bundle_archive = tmp_path / "bundle.zip"
+    with tarfile.open(source_archive, "w:gz") as archive:
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/CMakeLists.txt",
+            b"cmake_minimum_required(VERSION 3.14)\n",
+        )
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/convert_hf_to_gguf.py",
+            b"#!/usr/bin/env python3\nimport gguf\n",
+        )
+        add_bytes_to_tar(
+            archive,
+            f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py",
+            b"__all__ = []\n",
+        )
+    with zipfile.ZipFile(bundle_archive, "w") as archive:
+        archive.writestr("llama-server.exe", b"MZ")
+        archive.writestr("llama-quantize.exe", b"MZ")
+        archive.writestr("llama.dll", b"DLL")
+        archive.writestr("BUILD_INFO.txt", b"bundle metadata\n")
+
+    source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag))
+
+    def fake_download_file(url: str, destination: Path) -> None:
+        if url in source_urls:
+            destination.write_bytes(source_archive.read_bytes())
+            return
+        if url == "file://bundle.zip":
+            destination.write_bytes(bundle_archive.read_bytes())
+            return
+        raise AssertionError(f"unexpected download url: {url}")
+
+    monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "download_file", fake_download_file)
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "download_bytes",
+        lambda url, **_: b"#!/usr/bin/env python3\nimport gguf\n",
+    )
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "preflight_linux_installed_binaries",
+        lambda *args, **kwargs: None,
+    )
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT, "validate_quantize", lambda *args, **kwargs: None
+    )
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT, "validate_server", lambda *args, **kwargs: None
+    )
+
+    host = HostInfo(
+        system = "Windows",
+        machine = "AMD64",
+        is_windows = True,
+        is_linux = False,
+        is_macos = False,
+        is_x86_64 = True,
+        is_arm64 = False,
+        nvidia_smi = None,
+        driver_cuda_version = None,
+        compute_caps = [],
+        visible_cuda_devices = None,
+        has_physical_nvidia = False,
+        has_usable_nvidia = False,
+    )
+    choice = AssetChoice(
+        repo = "local",
+        tag = upstream_tag,
+        name = bundle_name,
+        url = "file://bundle.zip",
+        source_label = "local",
+        is_ready_bundle = True,
+        install_kind = "windows-cpu",
+        expected_sha256 = sha256_file(bundle_archive),
+    )
+
+    install_dir = tmp_path / "install"
+    work_dir = tmp_path / "work"
+    work_dir.mkdir()
+    probe_path = tmp_path / "stories260K.gguf"
+    quantized_path = tmp_path / "stories260K-q4.gguf"
+    validate_prebuilt_choice(
+        choice,
+        host,
+        install_dir,
+        work_dir,
+        probe_path,
+        requested_tag = upstream_tag,
+        llama_tag = upstream_tag,
+        approved_checksums = approved_checksums_for(
+            upstream_tag,
+            source_archive = source_archive,
+            bundle_archive = bundle_archive,
+            bundle_name = bundle_name,
+        ),
+        prebuilt_fallback_used = False,
+        quantized_path = quantized_path,
+    )
+
+    assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists()
+    assert (install_dir / "convert_hf_to_gguf.py").exists()
+    assert (install_dir / "build" / "bin" / "Release" / "llama-server.exe").exists()
+    assert (install_dir / "build" / "bin" / "Release" / "llama-quantize.exe").exists()
+    assert (install_dir / "build" / "bin" / "Release" / "llama.dll").exists()
+    assert not (install_dir / "llama-server.exe").exists()
+    assert (install_dir / "UNSLOTH_PREBUILT_INFO.json").exists()
+    assert (install_dir / "BUILD_INFO.txt").exists()
+
+
+def test_activate_install_tree_restores_existing_install_after_activation_failure(
+    tmp_path: Path,
+    monkeypatch: pytest.MonkeyPatch,
+    capsys: pytest.CaptureFixture[str],
+):
+    install_dir = tmp_path / "llama.cpp"
+    install_dir.mkdir()
+    (install_dir / "old.txt").write_text("old install\n")
+
+    staging_dir = create_install_staging_dir(install_dir)
+    (staging_dir / "new.txt").write_text("new install\n")
+
+    host = HostInfo(
+        system = "Linux",
+        machine = "x86_64",
+        is_windows = False,
+        is_linux = True,
+        is_macos = False,
+        is_x86_64 = True,
+        is_arm64 = False,
+        nvidia_smi = None,
+        driver_cuda_version = None,
+        compute_caps = [],
+        visible_cuda_devices = None,
+        has_physical_nvidia = False,
+        has_usable_nvidia = False,
+    )
+
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "confirm_install_tree",
+        lambda *_args, **_kwargs: (_ for _ in ()).throw(
+            RuntimeError("activation confirm failed")
+        ),
+    )
+
+    with pytest.raises(
+        PrebuiltFallback,
+        match = "activation failed; restored previous install",
+    ):
+        activate_install_tree(staging_dir, install_dir, host)
+
+    assert (install_dir / "old.txt").read_text() == "old install\n"
+    assert not (install_dir / "new.txt").exists()
+    assert not staging_dir.exists()
+    assert not (tmp_path / ".staging").exists()
+
+    output = capsys.readouterr().out
+    assert "moving existing install to rollback path" in output
+    assert "restored previous install from rollback path" in output
+
+
+def test_activate_install_tree_cleans_all_paths_when_rollback_restore_fails(
+    tmp_path: Path,
+    monkeypatch: pytest.MonkeyPatch,
+    capsys: pytest.CaptureFixture[str],
+):
+    install_dir = tmp_path / "llama.cpp"
+    install_dir.mkdir()
+    (install_dir / "old.txt").write_text("old install\n")
+
+    staging_dir = create_install_staging_dir(install_dir)
+    (staging_dir / "new.txt").write_text("new install\n")
+
+    host = HostInfo(
+        system = "Linux",
+        machine = "x86_64",
+        is_windows = False,
+        is_linux = True,
+        is_macos = False,
+        is_x86_64 = True,
+        is_arm64 = False,
+        nvidia_smi = None,
+        driver_cuda_version = None,
+        compute_caps = [],
+        visible_cuda_devices = None,
+        has_physical_nvidia = False,
+        has_usable_nvidia = False,
+    )
+
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "confirm_install_tree",
+        lambda *_args, **_kwargs: (_ for _ in ()).throw(
+            RuntimeError("activation confirm failed")
+        ),
+    )
+
+    original_replace = INSTALL_LLAMA_PREBUILT.os.replace
+
+    def flaky_replace(src, dst):
+        src_path = Path(src)
+        dst_path = Path(dst)
+        if "rollback-" in src_path.name and dst_path == install_dir:
+            raise OSError("restore failed")
+        return original_replace(src, dst)
+
+    monkeypatch.setattr(INSTALL_LLAMA_PREBUILT.os, "replace", flaky_replace)
+
+    with pytest.raises(
+        PrebuiltFallback,
+        match = "activation and rollback failed; cleaned install state for fresh source build",
+    ):
+        activate_install_tree(staging_dir, install_dir, host)
+
+    assert not install_dir.exists()
+    assert not staging_dir.exists()
+    assert not (tmp_path / ".staging").exists()
+
+    output = capsys.readouterr().out
+    assert "rollback after failed activation also failed: restore failed" in output
+    assert (
+        "cleaning staging, install, and rollback paths before source build fallback"
+        in output
+    )
+    assert "removing failed install path" in output
+    assert "removing rollback path" in output
+
+
+def test_binary_env_linux_includes_binary_parent_in_ld_library_path(
+    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+):
+    install_dir = tmp_path / "llama.cpp"
+    bin_dir = install_dir / "build" / "bin"
+    bin_dir.mkdir(parents = True)
+    binary_path = bin_dir / "llama-server"
+    binary_path.write_bytes(b"fake")
+
+    host = HostInfo(
+        system = "Linux",
+        machine = "x86_64",
+        is_windows = False,
+        is_linux = True,
+        is_macos = False,
+        is_x86_64 = True,
+        is_arm64 = False,
+        nvidia_smi = None,
+        driver_cuda_version = None,
+        compute_caps = [],
+        visible_cuda_devices = None,
+        has_physical_nvidia = False,
+        has_usable_nvidia = False,
+    )
+
+    monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "linux_runtime_dirs", lambda _bp: [])
+
+    env = binary_env(binary_path, install_dir, host)
+    ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep)
+    assert (
+        str(bin_dir) in ld_dirs
+    ), f"binary_path.parent ({bin_dir}) must be in LD_LIBRARY_PATH, got: {ld_dirs}"
+    assert str(install_dir) in ld_dirs
+
+
+def io_bytes(data: bytes):
+    return io.BytesIO(data)
+
+
+def add_bytes_to_tar(
+    archive: tarfile.TarFile, name: str, data: bytes, *, mode: int = 0o644
+) -> None:
+    info = tarfile.TarInfo(name)
+    info.size = len(data)
+    info.mode = mode
+    archive.addfile(info, io_bytes(data))
+
+
+def add_symlink_to_tar(archive: tarfile.TarFile, name: str, target: str) -> None:
+    info = tarfile.TarInfo(name)
+    info.type = tarfile.SYMTYPE
+    info.linkname = target
+    archive.addfile(info)
diff --git a/tests/studio/install/test_pr4562_bugfixes.py b/tests/studio/install/test_pr4562_bugfixes.py
new file mode 100644
index 0000000000..9b8c6219de
--- /dev/null
+++ b/tests/studio/install/test_pr4562_bugfixes.py
@@ -0,0 +1,687 @@
+"""
+Comprehensive tests for PR #4562 bug fixes.
+
+Tests cover:
+  - Bug 1: PS1 detached HEAD on re-run (fetch + checkout -B pattern)
+  - Bug 2: Source-build fallback ignores pinned tag (both .sh and .ps1)
+  - Bug 3: Unix fallback deletes install before checking prerequisites
+  - Bug 4: Linux LD_LIBRARY_PATH missing build/bin
+  - "latest" tag resolution fallback chain (Unsloth -> ggml-org -> raw)
+  - Cross-platform binary_env (Linux, macOS, Windows)
+  - Edge cases: malformed JSON, empty responses, env overrides
+
+Run: pytest tests/studio/install/test_pr4562_bugfixes.py -v
+"""
+
+import importlib.util
+import json
+import os
+import subprocess
+import sys
+import textwrap
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+
+# ---------------------------------------------------------------------------
+# Load the module under test (same pattern as existing test files)
+# ---------------------------------------------------------------------------
+PACKAGE_ROOT = Path(__file__).resolve().parents[3]
+MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py"
+SPEC = importlib.util.spec_from_file_location(
+    "studio_install_llama_prebuilt", MODULE_PATH
+)
+assert SPEC is not None and SPEC.loader is not None
+MOD = importlib.util.module_from_spec(SPEC)
+sys.modules[SPEC.name] = MOD
+SPEC.loader.exec_module(MOD)
+
+binary_env = MOD.binary_env
+HostInfo = MOD.HostInfo
+resolve_requested_llama_tag = MOD.resolve_requested_llama_tag
+
+SETUP_SH = PACKAGE_ROOT / "studio" / "setup.sh"
+SETUP_PS1 = PACKAGE_ROOT / "studio" / "setup.ps1"
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+def make_host(*, system: str) -> HostInfo:
+    """Create a HostInfo for the given OS."""
+    return HostInfo(
+        system = system,
+        machine = "x86_64" if system != "Darwin" else "arm64",
+        is_windows = (system == "Windows"),
+        is_linux = (system == "Linux"),
+        is_macos = (system == "Darwin"),
+        is_x86_64 = (system != "Darwin"),
+        is_arm64 = (system == "Darwin"),
+        nvidia_smi = None,
+        driver_cuda_version = None,
+        compute_caps = [],
+        visible_cuda_devices = None,
+        has_physical_nvidia = False,
+        has_usable_nvidia = False,
+    )
+
+
+BASH = "/bin/bash"
+
+
+def run_bash(script: str, *, timeout: int = 10, env: dict | None = None) -> str:
+    """Run a bash script fragment and return its stdout."""
+    run_env = os.environ.copy()
+    if env:
+        run_env.update(env)
+    result = subprocess.run(
+        [BASH, "-c", script],
+        capture_output = True,
+        text = True,
+        timeout = timeout,
+        env = run_env,
+    )
+    return result.stdout.strip()
+
+
+# =========================================================================
+# TEST GROUP A: binary_env across all platforms (Bug 4 + cross-platform)
+# =========================================================================
+class TestBinaryEnvCrossPlatform:
+    """Test that binary_env returns correct library paths for all OSes."""
+
+    def test_linux_includes_binary_parent_in_ld_library_path(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+    ):
+        install_dir = tmp_path / "llama.cpp"
+        bin_dir = install_dir / "build" / "bin"
+        bin_dir.mkdir(parents = True)
+        binary_path = bin_dir / "llama-server"
+        binary_path.write_bytes(b"fake")
+
+        host = make_host(system = "Linux")
+        monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: [])
+
+        env = binary_env(binary_path, install_dir, host)
+        ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep)
+        assert str(bin_dir) in ld_dirs, f"build/bin not in LD_LIBRARY_PATH: {ld_dirs}"
+        assert (
+            str(install_dir) in ld_dirs
+        ), f"install_dir not in LD_LIBRARY_PATH: {ld_dirs}"
+
+    def test_linux_binary_parent_comes_before_install_dir(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+    ):
+        """build/bin should be searched before install_dir for .so files."""
+        install_dir = tmp_path / "llama.cpp"
+        bin_dir = install_dir / "build" / "bin"
+        bin_dir.mkdir(parents = True)
+        binary_path = bin_dir / "llama-server"
+        binary_path.write_bytes(b"fake")
+
+        host = make_host(system = "Linux")
+        monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: [])
+
+        env = binary_env(binary_path, install_dir, host)
+        ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep)
+        bin_idx = ld_dirs.index(str(bin_dir))
+        install_idx = ld_dirs.index(str(install_dir))
+        assert (
+            bin_idx < install_idx
+        ), "binary_path.parent should come before install_dir"
+
+    def test_linux_deduplicates_when_binary_parent_equals_install_dir(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+    ):
+        """When binary is directly in install_dir, no duplicate entries."""
+        install_dir = tmp_path / "llama.cpp"
+        install_dir.mkdir(parents = True)
+        binary_path = install_dir / "llama-server"
+        binary_path.write_bytes(b"fake")
+
+        host = make_host(system = "Linux")
+        monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: [])
+
+        env = binary_env(binary_path, install_dir, host)
+        ld_dirs = [d for d in env["LD_LIBRARY_PATH"].split(os.pathsep) if d]
+        count = ld_dirs.count(str(install_dir))
+        assert count == 1, f"install_dir appears {count} times in LD_LIBRARY_PATH"
+
+    def test_linux_preserves_existing_ld_library_path(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+    ):
+        install_dir = tmp_path / "llama.cpp"
+        bin_dir = install_dir / "build" / "bin"
+        bin_dir.mkdir(parents = True)
+        binary_path = bin_dir / "llama-server"
+        binary_path.write_bytes(b"fake")
+
+        # Create real directories so dedupe_existing_dirs keeps them
+        custom_lib = tmp_path / "custom_lib"
+        other_lib = tmp_path / "other_lib"
+        custom_lib.mkdir()
+        other_lib.mkdir()
+
+        host = make_host(system = "Linux")
+        monkeypatch.setattr(MOD, "linux_runtime_dirs", lambda _bp: [])
+        original = os.environ.get("LD_LIBRARY_PATH", "")
+        os.environ["LD_LIBRARY_PATH"] = f"{custom_lib}:{other_lib}"
+        try:
+            env = binary_env(binary_path, install_dir, host)
+        finally:
+            if original:
+                os.environ["LD_LIBRARY_PATH"] = original
+            else:
+                os.environ.pop("LD_LIBRARY_PATH", None)
+        ld_dirs = env["LD_LIBRARY_PATH"].split(os.pathsep)
+        assert str(custom_lib.resolve()) in ld_dirs
+        assert str(other_lib.resolve()) in ld_dirs
+
+    def test_windows_includes_binary_parent_in_path(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+    ):
+        install_dir = tmp_path / "llama.cpp"
+        bin_dir = install_dir / "build" / "bin" / "Release"
+        bin_dir.mkdir(parents = True)
+        binary_path = bin_dir / "llama-server.exe"
+        binary_path.write_bytes(b"MZ")
+
+        host = make_host(system = "Windows")
+        monkeypatch.setattr(
+            MOD, "windows_runtime_dirs_for_runtime_line", lambda _rt: []
+        )
+
+        env = binary_env(binary_path, install_dir, host)
+        path_dirs = env["PATH"].split(os.pathsep)
+        assert str(bin_dir) in path_dirs, f"build/bin/Release not in PATH: {path_dirs}"
+
+    def test_macos_sets_dyld_library_path(
+        self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+    ):
+        install_dir = tmp_path / "llama.cpp"
+        install_dir.mkdir(parents = True)
+        bin_dir = install_dir / "build" / "bin"
+        binary_path = bin_dir / "llama-server"
+        binary_path.parent.mkdir(parents = True)
+        binary_path.write_bytes(b"fake")
+
+        host = make_host(system = "Darwin")
+        monkeypatch.delenv("DYLD_LIBRARY_PATH", raising = False)
+
+        env = binary_env(binary_path, install_dir, host)
+        dyld_parts = [p for p in env["DYLD_LIBRARY_PATH"].split(os.pathsep) if p]
+        assert (
+            str(bin_dir) in dyld_parts
+        ), f"build/bin not in DYLD_LIBRARY_PATH: {dyld_parts}"
+        assert (
+            str(install_dir) in dyld_parts
+        ), f"install_dir not in DYLD_LIBRARY_PATH: {dyld_parts}"
+        # binary_path.parent (build/bin) should come before install_dir
+        assert dyld_parts.index(str(bin_dir)) < dyld_parts.index(str(install_dir))
+
+
+# =========================================================================
+# TEST GROUP B: resolve_requested_llama_tag (Python function)
+# =========================================================================
+class TestResolveRequestedLlamaTag:
+    def test_concrete_tag_passes_through(self):
+        assert resolve_requested_llama_tag("b8508") == "b8508"
+
+    def test_none_resolves_to_latest(self, monkeypatch: pytest.MonkeyPatch):
+        monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b9999")
+        assert resolve_requested_llama_tag(None) == "b9999"
+
+    def test_latest_resolves_to_upstream(self, monkeypatch: pytest.MonkeyPatch):
+        monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b1234")
+        assert resolve_requested_llama_tag("latest") == "b1234"
+
+    def test_empty_string_resolves_to_latest(self, monkeypatch: pytest.MonkeyPatch):
+        monkeypatch.setattr(MOD, "latest_upstream_release_tag", lambda: "b5555")
+        assert resolve_requested_llama_tag("") == "b5555"
+
+
+# =========================================================================
+# TEST GROUP C: setup.sh logic (bash subprocess tests)
+# =========================================================================
+class TestSetupShLogic:
+    """Test setup.sh fragments via bash subprocess with controlled PATH."""
+
+    def test_cmake_missing_preserves_install(self, tmp_path: Path):
+        """Bug 3: When cmake is missing, rm -rf should NOT run."""
+        llama_dir = tmp_path / "llama.cpp"
+        llama_dir.mkdir()
+        marker = llama_dir / "marker.txt"
+        marker.write_text("existing")
+
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir()
+        # Create mock git but NOT cmake
+        (mock_bin / "git").write_text("#!/bin/bash\nexit 0\n")
+        (mock_bin / "git").chmod(0o755)
+
+        # Build PATH: mock_bin first, then system dirs WITHOUT cmake
+        safe_dirs = [str(mock_bin)]
+        for d in os.environ.get("PATH", "").split(":"):
+            if d and not os.path.isfile(os.path.join(d, "cmake")):
+                safe_dirs.append(d)
+
+        script = textwrap.dedent(f"""\
+            export LLAMA_CPP_DIR="{llama_dir}"
+            if ! command -v cmake &>/dev/null; then
+                echo "cmake_missing"
+            elif ! command -v git &>/dev/null; then
+                echo "git_missing"
+            else
+                rm -rf "$LLAMA_CPP_DIR"
+                echo "would_clone"
+            fi
+        """)
+        output = run_bash(script, env = {"PATH": ":".join(safe_dirs)})
+        assert "cmake_missing" in output
+        assert marker.exists(), "Install dir was deleted despite cmake missing!"
+
+    def test_git_missing_preserves_install(self, tmp_path: Path):
+        """Bug 3: When git is missing, rm -rf should NOT run."""
+        llama_dir = tmp_path / "llama.cpp"
+        llama_dir.mkdir()
+        marker = llama_dir / "marker.txt"
+        marker.write_text("existing")
+
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir()
+        # Create mock cmake but NOT git
+        (mock_bin / "cmake").write_text("#!/bin/bash\nexit 0\n")
+        (mock_bin / "cmake").chmod(0o755)
+
+        # Build PATH: mock_bin first, then system dirs WITHOUT git
+        safe_dirs = [str(mock_bin)]
+        for d in os.environ.get("PATH", "").split(":"):
+            if d and not os.path.isfile(os.path.join(d, "git")):
+                safe_dirs.append(d)
+
+        script = textwrap.dedent(f"""\
+            export LLAMA_CPP_DIR="{llama_dir}"
+            if ! command -v cmake &>/dev/null; then
+                echo "cmake_missing"
+            elif ! command -v git &>/dev/null; then
+                echo "git_missing"
+            else
+                rm -rf "$LLAMA_CPP_DIR"
+                echo "would_clone"
+            fi
+        """)
+        output = run_bash(script, env = {"PATH": ":".join(safe_dirs)})
+        assert "git_missing" in output
+        assert marker.exists(), "Install dir was deleted despite git missing!"
+
+    def test_both_present_runs_rm_and_clone(self, tmp_path: Path):
+        """Bug 3: When both present, rm -rf runs before clone."""
+        llama_dir = tmp_path / "llama.cpp"
+        llama_dir.mkdir()
+        marker = llama_dir / "marker.txt"
+        marker.write_text("existing")
+
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir()
+        (mock_bin / "cmake").write_text("#!/bin/bash\nexit 0\n")
+        (mock_bin / "cmake").chmod(0o755)
+        (mock_bin / "git").write_text("#!/bin/bash\nexit 0\n")
+        (mock_bin / "git").chmod(0o755)
+
+        script = textwrap.dedent(f"""\
+            export PATH="{mock_bin}:$PATH"
+            export LLAMA_CPP_DIR="{llama_dir}"
+            if ! command -v cmake &>/dev/null; then
+                echo "cmake_missing"
+            elif ! command -v git &>/dev/null; then
+                echo "git_missing"
+            else
+                rm -rf "$LLAMA_CPP_DIR"
+                echo "would_clone"
+            fi
+        """)
+        output = run_bash(script)
+        assert "would_clone" in output
+        assert not marker.exists(), "Install dir should have been deleted"
+
+    def test_clone_uses_pinned_tag(self, tmp_path: Path):
+        """Bug 2: git clone should use --branch with the resolved tag."""
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir()
+        log_file = tmp_path / "git_calls.log"
+        (mock_bin / "git").write_text(f'#!/bin/bash\necho "$*" >> {log_file}\nexit 0\n')
+        (mock_bin / "git").chmod(0o755)
+
+        script = textwrap.dedent(f"""\
+            export PATH="{mock_bin}:$PATH"
+            git clone --depth 1 --branch "b8508" https://github.com/ggml-org/llama.cpp.git /tmp/llama_test
+        """)
+        run_bash(script)
+        log = log_file.read_text()
+        assert "--branch b8508" in log, f"Expected --branch b8508 in: {log}"
+
+    def test_fetch_checkout_b_pattern(self, tmp_path: Path):
+        """Bug 1: Re-run should use fetch + checkout -B, not pull + checkout FETCH_HEAD."""
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir()
+        log_file = tmp_path / "git_calls.log"
+        (mock_bin / "git").write_text(f'#!/bin/bash\necho "$*" >> {log_file}\nexit 0\n')
+        (mock_bin / "git").chmod(0o755)
+
+        llama_dir = tmp_path / "llama.cpp"
+        llama_dir.mkdir()
+        (llama_dir / ".git").mkdir()
+
+        script = textwrap.dedent(f"""\
+            export PATH="{mock_bin}:$PATH"
+            LlamaCppDir="{llama_dir}"
+            ResolvedLlamaTag="b8508"
+            if [ -d "$LlamaCppDir/.git" ]; then
+                git -C "$LlamaCppDir" fetch --depth 1 origin "$ResolvedLlamaTag"
+                if [ $? -ne 0 ]; then
+                    echo "WARN: fetch failed"
+                else
+                    git -C "$LlamaCppDir" checkout -B unsloth-llama-build FETCH_HEAD
+                fi
+            fi
+        """)
+        run_bash(script)
+        log = log_file.read_text()
+        assert "fetch --depth 1 origin b8508" in log
+        assert "checkout -B unsloth-llama-build FETCH_HEAD" in log
+        assert "pull" not in log, "Should use fetch, not pull"
+
+    def test_fetch_failure_warns_not_aborts(self, tmp_path: Path):
+        """Bug 1: fetch failure should warn and continue, not set BuildOk=false."""
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir()
+        (mock_bin / "git").write_text(
+            '#!/bin/bash\nif echo "$*" | grep -q fetch; then exit 1; fi\nexit 0\n'
+        )
+        (mock_bin / "git").chmod(0o755)
+
+        llama_dir = tmp_path / "llama.cpp"
+        llama_dir.mkdir()
+        (llama_dir / ".git").mkdir()
+
+        script = textwrap.dedent(f"""\
+            export PATH="{mock_bin}:$PATH"
+            LlamaCppDir="{llama_dir}"
+            ResolvedLlamaTag="b8508"
+            BuildOk=true
+            if [ -d "$LlamaCppDir/.git" ]; then
+                git -C "$LlamaCppDir" fetch --depth 1 origin "$ResolvedLlamaTag"
+                if [ $? -ne 0 ]; then
+                    echo "WARN: fetch failed -- using existing source"
+                else
+                    git -C "$LlamaCppDir" checkout -B unsloth-llama-build FETCH_HEAD
+                fi
+            fi
+            echo "BuildOk=$BuildOk"
+        """)
+        output = run_bash(script)
+        assert "WARN: fetch failed" in output
+        assert "BuildOk=true" in output
+
+
+# =========================================================================
+# TEST GROUP D: "latest" tag resolution (bash subprocess)
+# =========================================================================
+class TestLatestTagResolution:
+    """Test the fallback chain: Unsloth API -> ggml-org API -> raw."""
+
+    RESOLVE_TEMPLATE = textwrap.dedent("""\
+        export PATH="{mock_bin}:$PATH"
+        _REQUESTED_LLAMA_TAG="{requested_tag}"
+        _RESOLVED_LLAMA_TAG=""
+        _RESOLVE_UPSTREAM_STATUS=1
+        _HELPER_RELEASE_REPO="unslothai/llama.cpp"
+        if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+            if [ "$_REQUESTED_LLAMA_TAG" = "latest" ]; then
+                _RESOLVED_LLAMA_TAG="$(curl -fsSL "https://api.github.com/repos/${{_HELPER_RELEASE_REPO}}/releases/latest" 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
+                if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+                    _RESOLVED_LLAMA_TAG="$(curl -fsSL https://api.github.com/repos/ggml-org/llama.cpp/releases/latest 2>/dev/null | python -c "import sys,json; print(json.load(sys.stdin)['tag_name'])" 2>/dev/null)" || _RESOLVED_LLAMA_TAG=""
+                fi
+            fi
+            if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
+                _RESOLVED_LLAMA_TAG="$_REQUESTED_LLAMA_TAG"
+            fi
+        fi
+        echo "$_RESOLVED_LLAMA_TAG"
+    """)
+
+    @staticmethod
+    def _make_curl_mock(
+        mock_bin: Path, unsloth_response: str | None, ggml_response: str | None
+    ):
+        """Create a curl mock that returns different responses per repo."""
+        lines = ["#!/bin/bash"]
+        if unsloth_response is not None:
+            lines.append(
+                f'if echo "$*" | grep -q "unslothai/llama.cpp"; then echo \'{unsloth_response}\'; exit 0; fi'
+            )
+        else:
+            lines.append(
+                'if echo "$*" | grep -q "unslothai/llama.cpp"; then exit 1; fi'
+            )
+        if ggml_response is not None:
+            lines.append(
+                f'if echo "$*" | grep -q "ggml-org/llama.cpp"; then echo \'{ggml_response}\'; exit 0; fi'
+            )
+        else:
+            lines.append('if echo "$*" | grep -q "ggml-org/llama.cpp"; then exit 1; fi')
+        lines.append("exit 1")
+        curl_path = mock_bin / "curl"
+        curl_path.write_text("\n".join(lines) + "\n")
+        curl_path.chmod(0o755)
+
+    def _run_resolve(
+        self,
+        tmp_path: Path,
+        requested_tag: str,
+        unsloth_resp: str | None,
+        ggml_resp: str | None,
+    ) -> str:
+        mock_bin = tmp_path / "mock_bin"
+        mock_bin.mkdir(exist_ok = True)
+        self._make_curl_mock(mock_bin, unsloth_resp, ggml_resp)
+        script = self.RESOLVE_TEMPLATE.format(
+            mock_bin = mock_bin, requested_tag = requested_tag
+        )
+        return run_bash(script)
+
+    def test_unsloth_succeeds(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = '{"tag_name":"b8508"}',
+            ggml_resp = '{"tag_name":"b9000"}',
+        )
+        assert output == "b8508"
+
+    def test_unsloth_fails_ggml_succeeds(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = None,
+            ggml_resp = '{"tag_name":"b9000"}',
+        )
+        assert output == "b9000"
+
+    def test_both_fail_raw_fallback(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = None,
+            ggml_resp = None,
+        )
+        assert output == "latest"
+
+    def test_concrete_tag_passes_through(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "b7777",
+            unsloth_resp = '{"tag_name":"b8508"}',
+            ggml_resp = '{"tag_name":"b9000"}',
+        )
+        assert output == "b7777"
+
+    def test_unsloth_malformed_json_falls_through(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = '{"bad_key":"no_tag"}',
+            ggml_resp = '{"tag_name":"b9001"}',
+        )
+        assert output == "b9001"
+
+    def test_both_malformed_json_raw_fallback(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = '{"bad":"data"}',
+            ggml_resp = '{"also":"bad"}',
+        )
+        assert output == "latest"
+
+    def test_unsloth_empty_body_falls_through(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = "",
+            ggml_resp = '{"tag_name":"b7000"}',
+        )
+        assert output == "b7000"
+
+    def test_unsloth_empty_tag_name_falls_through(self, tmp_path: Path):
+        output = self._run_resolve(
+            tmp_path,
+            "latest",
+            unsloth_resp = '{"tag_name":""}',
+            ggml_resp = '{"tag_name":"b6000"}',
+        )
+        assert output == "b6000"
+
+    def test_env_override_unsloth_llama_tag(self):
+        output = run_bash(
+            'echo "${UNSLOTH_LLAMA_TAG:-latest}"',
+            env = {"UNSLOTH_LLAMA_TAG": "b1234"},
+        )
+        assert output == "b1234"
+
+    def test_env_unset_defaults_to_latest(self):
+        env = os.environ.copy()
+        env.pop("UNSLOTH_LLAMA_TAG", None)
+        output = run_bash('echo "${UNSLOTH_LLAMA_TAG:-latest}"', env = env)
+        assert output == "latest"
+
+    def test_env_empty_defaults_to_latest(self):
+        output = run_bash(
+            'echo "${UNSLOTH_LLAMA_TAG:-latest}"',
+            env = {"UNSLOTH_LLAMA_TAG": ""},
+        )
+        assert output == "latest"
+
+
+# =========================================================================
+# TEST GROUP E: Source file verification
+# =========================================================================
+class TestSourceCodePatterns:
+    """Verify the actual source files contain the expected fix patterns."""
+
+    def test_setup_sh_no_rm_before_prereq_check(self):
+        """rm -rf must appear AFTER cmake/git checks, not before."""
+        content = SETUP_SH.read_text()
+        # Find the source-build block
+        idx_else = content.find("# Check prerequisites")
+        assert idx_else != -1
+        block = content[idx_else:]
+        # rm -rf should appear after the cmake/git checks
+        idx_cmake = block.find("command -v cmake")
+        idx_git = block.find("command -v git")
+        idx_rm = block.find("rm -rf")
+        assert idx_rm > idx_cmake, "rm -rf should come after cmake check"
+        assert idx_rm > idx_git, "rm -rf should come after git check"
+
+    def test_setup_sh_clone_uses_branch_tag(self):
+        """git clone in source-build should use --branch via _CLONE_BRANCH_ARGS."""
+        content = SETUP_SH.read_text()
+        # The clone line should use _CLONE_BRANCH_ARGS (which conditionally includes --branch)
+        assert (
+            "_CLONE_BRANCH_ARGS" in content
+        ), "Clone should use _CLONE_BRANCH_ARGS array"
+        assert (
+            '--branch "$_RESOLVED_LLAMA_TAG"' in content
+        ), "_CLONE_BRANCH_ARGS should be set to --branch $_RESOLVED_LLAMA_TAG"
+        # Verify the guard: --branch is only used when tag is not "latest"
+        assert (
+            '_RESOLVED_LLAMA_TAG" != "latest"' in content
+        ), "Should guard against literal 'latest' tag"
+
+    def test_setup_sh_latest_resolution_queries_unsloth_first(self):
+        """The Unsloth repo should be queried before ggml-org."""
+        content = SETUP_SH.read_text()
+        idx_unsloth = content.find("_HELPER_RELEASE_REPO}/releases/latest")
+        idx_ggml = content.find("ggml-org/llama.cpp/releases/latest")
+        assert idx_unsloth != -1, "Unsloth API query not found"
+        assert idx_ggml != -1, "ggml-org API query not found"
+        assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org"
+
+    def test_setup_ps1_uses_checkout_b(self):
+        """PS1 should use checkout -B, not checkout --force FETCH_HEAD."""
+        content = SETUP_PS1.read_text()
+        assert "checkout -B unsloth-llama-build" in content
+        assert "checkout --force FETCH_HEAD" not in content
+
+    def test_setup_ps1_clone_uses_branch_tag(self):
+        """PS1 clone should use --branch with the resolved tag."""
+        content = SETUP_PS1.read_text()
+        assert "--branch" in content and "$ResolvedLlamaTag" in content
+        # The old commented-out line should be gone
+        assert "# git clone --depth 1 --branch" not in content
+
+    def test_setup_ps1_no_git_pull(self):
+        """PS1 should use fetch, not pull (which fails in detached HEAD)."""
+        content = SETUP_PS1.read_text()
+        # In the source-build section, there should be no "git pull"
+        # (git pull is only valid on a branch)
+        lines = content.splitlines()
+        for i, line in enumerate(lines):
+            stripped = line.strip()
+            if "git pull" in stripped and not stripped.startswith("#"):
+                # Check context -- should not be in the llama.cpp build section
+                # Allow git pull in other contexts
+                context = "\n".join(lines[max(0, i - 5) : i + 5])
+                if "LlamaCppDir" in context:
+                    pytest.fail(
+                        f"Found 'git pull' in llama.cpp build section at line {i+1}"
+                    )
+
+    def test_setup_ps1_latest_resolution_queries_unsloth_first(self):
+        """PS1 should query Unsloth repo before ggml-org."""
+        content = SETUP_PS1.read_text()
+        idx_unsloth = content.find("$HelperReleaseRepo/releases/latest")
+        idx_ggml = content.find("ggml-org/llama.cpp/releases/latest")
+        assert idx_unsloth != -1, "Unsloth API query not found in PS1"
+        assert idx_ggml != -1, "ggml-org API query not found in PS1"
+        assert idx_unsloth < idx_ggml, "Unsloth should be queried before ggml-org"
+
+    def test_binary_env_linux_has_binary_parent(self):
+        """The Linux branch of binary_env should include binary_path.parent."""
+        content = MODULE_PATH.read_text()
+        # Find the binary_env function
+        in_func = False
+        in_linux = False
+        found = False
+        for line in content.splitlines():
+            if "def binary_env(" in line:
+                in_func = True
+            elif in_func and line and not line[0].isspace() and "def " in line:
+                break
+            if in_func and "host.is_linux" in line:
+                in_linux = True
+            if in_linux and "binary_path.parent" in line:
+                found = True
+                break
+        assert found, "binary_path.parent not found in Linux branch of binary_env"
diff --git a/tests/studio/install/test_selection_logic.py b/tests/studio/install/test_selection_logic.py
new file mode 100644
index 0000000000..906c978b0d
--- /dev/null
+++ b/tests/studio/install/test_selection_logic.py
@@ -0,0 +1,903 @@
+"""Tests for binary selection logic in install_llama_prebuilt.py.
+
+Covers: normalize_compute_cap, normalize_compute_caps, parse_cuda_visible_devices,
+supports_explicit_visible_device_matching, select_visible_gpu_rows,
+compatible_linux_runtime_lines, pick_windows_cuda_runtime,
+compatible_windows_runtime_lines, runtime_line_from_cuda_version,
+apply_approved_hashes, linux_cuda_choice_from_release, windows_cuda_attempts,
+resolve_upstream_asset_choice.
+
+No GPU, no network, no torch required -- all I/O is monkeypatched.
+"""
+
+import importlib.util
+import sys
+from pathlib import Path
+
+import pytest
+
+
+PACKAGE_ROOT = Path(__file__).resolve().parents[3]
+MODULE_PATH = PACKAGE_ROOT / "studio" / "install_llama_prebuilt.py"
+SPEC = importlib.util.spec_from_file_location(
+    "studio_install_llama_prebuilt", MODULE_PATH
+)
+assert SPEC is not None and SPEC.loader is not None
+INSTALL_LLAMA_PREBUILT = importlib.util.module_from_spec(SPEC)
+sys.modules[SPEC.name] = INSTALL_LLAMA_PREBUILT
+SPEC.loader.exec_module(INSTALL_LLAMA_PREBUILT)
+
+HostInfo = INSTALL_LLAMA_PREBUILT.HostInfo
+AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice
+PublishedLlamaArtifact = INSTALL_LLAMA_PREBUILT.PublishedLlamaArtifact
+PublishedReleaseBundle = INSTALL_LLAMA_PREBUILT.PublishedReleaseBundle
+ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash
+ApprovedReleaseChecksums = INSTALL_LLAMA_PREBUILT.ApprovedReleaseChecksums
+PrebuiltFallback = INSTALL_LLAMA_PREBUILT.PrebuiltFallback
+LinuxCudaSelection = INSTALL_LLAMA_PREBUILT.LinuxCudaSelection
+UPSTREAM_REPO = INSTALL_LLAMA_PREBUILT.UPSTREAM_REPO
+
+normalize_compute_cap = INSTALL_LLAMA_PREBUILT.normalize_compute_cap
+normalize_compute_caps = INSTALL_LLAMA_PREBUILT.normalize_compute_caps
+parse_cuda_visible_devices = INSTALL_LLAMA_PREBUILT.parse_cuda_visible_devices
+supports_explicit_visible_device_matching = (
+    INSTALL_LLAMA_PREBUILT.supports_explicit_visible_device_matching
+)
+select_visible_gpu_rows = INSTALL_LLAMA_PREBUILT.select_visible_gpu_rows
+compatible_linux_runtime_lines = INSTALL_LLAMA_PREBUILT.compatible_linux_runtime_lines
+pick_windows_cuda_runtime = INSTALL_LLAMA_PREBUILT.pick_windows_cuda_runtime
+compatible_windows_runtime_lines = (
+    INSTALL_LLAMA_PREBUILT.compatible_windows_runtime_lines
+)
+runtime_line_from_cuda_version = INSTALL_LLAMA_PREBUILT.runtime_line_from_cuda_version
+apply_approved_hashes = INSTALL_LLAMA_PREBUILT.apply_approved_hashes
+linux_cuda_choice_from_release = INSTALL_LLAMA_PREBUILT.linux_cuda_choice_from_release
+windows_cuda_attempts = INSTALL_LLAMA_PREBUILT.windows_cuda_attempts
+resolve_upstream_asset_choice = INSTALL_LLAMA_PREBUILT.resolve_upstream_asset_choice
+
+
+# ---------------------------------------------------------------------------
+# Helper factories
+# ---------------------------------------------------------------------------
+
+
+def make_host(**overrides):
+    system = overrides.pop("system", "Linux")
+    machine = overrides.pop("machine", "x86_64")
+    defaults = dict(
+        system = system,
+        machine = machine,
+        is_linux = system == "Linux",
+        is_windows = system == "Windows",
+        is_macos = system == "Darwin",
+        is_x86_64 = machine.lower() in {"x86_64", "amd64"},
+        is_arm64 = machine.lower() in {"arm64", "aarch64"},
+        nvidia_smi = "/usr/bin/nvidia-smi",
+        driver_cuda_version = (12, 8),
+        compute_caps = ["86"],
+        visible_cuda_devices = None,
+        has_physical_nvidia = True,
+        has_usable_nvidia = True,
+    )
+    defaults.update(overrides)
+    return HostInfo(**defaults)
+
+
+def make_artifact(asset_name, **overrides):
+    defaults = dict(
+        asset_name = asset_name,
+        install_kind = "linux-cuda",
+        runtime_line = "cuda12",
+        coverage_class = "targeted",
+        supported_sms = ["75", "80", "86", "89", "90"],
+        min_sm = 75,
+        max_sm = 90,
+        bundle_profile = "cuda12-newer",
+        rank = 100,
+    )
+    defaults.update(overrides)
+    return PublishedLlamaArtifact(**defaults)
+
+
+def make_release(artifacts, **overrides):
+    defaults = dict(
+        repo = "unslothai/llama.cpp",
+        release_tag = "v1.0",
+        upstream_tag = "b8508",
+        assets = {a.asset_name: f"https://example.com/{a.asset_name}" for a in artifacts},
+        manifest_asset_name = "llama-prebuilt-manifest.json",
+        artifacts = artifacts,
+        selection_log = [],
+    )
+    defaults.update(overrides)
+    return PublishedReleaseBundle(**defaults)
+
+
+def make_checksums(asset_names):
+    return ApprovedReleaseChecksums(
+        repo = "unslothai/llama.cpp",
+        release_tag = "v1.0",
+        upstream_tag = "b8508",
+        source_commit = None,
+        artifacts = {
+            name: ApprovedArtifactHash(
+                asset_name = name,
+                sha256 = "a" * 64,
+                repo = "unslothai/llama.cpp",
+                kind = "prebuilt",
+            )
+            for name in asset_names
+        },
+    )
+
+
+def mock_linux_runtime(monkeypatch, lines):
+    dirs = {line: ["/usr/lib/stub"] for line in lines}
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "detected_linux_runtime_lines",
+        lambda: (list(lines), dict(dirs)),
+    )
+
+
+def mock_windows_runtime(monkeypatch, lines):
+    dirs = {line: ["C:\\Windows\\System32"] for line in lines}
+    monkeypatch.setattr(
+        INSTALL_LLAMA_PREBUILT,
+        "detected_windows_runtime_lines",
+        lambda: (list(lines), dict(dirs)),
+    )
+
+
+# ===========================================================================
+# A. normalize_compute_cap
+# ===========================================================================
+
+
+class TestNormalizeComputeCap:
+    def test_dotted_86(self):
+        assert normalize_compute_cap("8.6") == "86"
+
+    def test_dotted_leading_zero(self):
+        assert normalize_compute_cap("07.05") == "75"
+
+    def test_already_normalized(self):
+        assert normalize_compute_cap("75") == "75"
+
+    def test_int_input(self):
+        assert normalize_compute_cap(86) == "86"
+
+    def test_empty_string(self):
+        assert normalize_compute_cap("") is None
+
+    def test_whitespace(self):
+        assert normalize_compute_cap("  ") is None
+
+    def test_non_numeric(self):
+        assert normalize_compute_cap("x.y") is None
+
+    def test_triple_part(self):
+        assert normalize_compute_cap("8.6.0") is None
+
+    def test_zero_minor(self):
+        assert normalize_compute_cap("9.0") == "90"
+
+
+# ===========================================================================
+# B. normalize_compute_caps
+# ===========================================================================
+
+
+class TestNormalizeComputeCaps:
+    def test_deduplication(self):
+        assert normalize_compute_caps(["8.6", "86", "8.6"]) == ["86"]
+
+    def test_numeric_sort(self):
+        assert normalize_compute_caps(["9.0", "7.5", "8.6"]) == ["75", "86", "90"]
+
+    def test_drops_invalid(self):
+        assert normalize_compute_caps(["8.6", "bad", "", "7.5"]) == ["75", "86"]
+
+    def test_empty_input(self):
+        assert normalize_compute_caps([]) == []
+
+
+# ===========================================================================
+# C. parse_cuda_visible_devices
+# ===========================================================================
+
+
+class TestParseCudaVisibleDevices:
+    def test_none(self):
+        assert parse_cuda_visible_devices(None) is None
+
+    def test_empty(self):
+        assert parse_cuda_visible_devices("") == []
+
+    def test_minus_one(self):
+        assert parse_cuda_visible_devices("-1") == []
+
+    def test_single(self):
+        assert parse_cuda_visible_devices("0") == ["0"]
+
+    def test_multi(self):
+        assert parse_cuda_visible_devices("0,1,2") == ["0", "1", "2"]
+
+    def test_whitespace_stripped(self):
+        assert parse_cuda_visible_devices(" 0 , 1 ") == ["0", "1"]
+
+
+# ===========================================================================
+# D. supports_explicit_visible_device_matching
+# ===========================================================================
+
+
+class TestSupportsExplicitVisibleDeviceMatching:
+    def test_all_digits(self):
+        assert supports_explicit_visible_device_matching(["0", "1", "2"]) is True
+
+    def test_gpu_prefix(self):
+        assert supports_explicit_visible_device_matching(["GPU-abc123"]) is True
+
+    def test_none(self):
+        assert supports_explicit_visible_device_matching(None) is False
+
+    def test_empty(self):
+        assert supports_explicit_visible_device_matching([]) is False
+
+    def test_mixed_invalid(self):
+        assert supports_explicit_visible_device_matching(["0", "MIG-device"]) is False
+
+
+# ===========================================================================
+# E. select_visible_gpu_rows
+# ===========================================================================
+
+
+class TestSelectVisibleGpuRows:
+    ROWS = [
+        ("0", "GPU-aaa", "8.6"),
+        ("1", "GPU-bbb", "7.5"),
+        ("2", "GPU-ccc", "8.9"),
+    ]
+
+    def test_none_returns_all(self):
+        assert select_visible_gpu_rows(self.ROWS, None) == list(self.ROWS)
+
+    def test_empty_returns_empty(self):
+        assert select_visible_gpu_rows(self.ROWS, []) == []
+
+    def test_filter_by_index(self):
+        result = select_visible_gpu_rows(self.ROWS, ["0", "2"])
+        assert result == [("0", "GPU-aaa", "8.6"), ("2", "GPU-ccc", "8.9")]
+
+    def test_filter_by_uuid_case_insensitive(self):
+        result = select_visible_gpu_rows(self.ROWS, ["gpu-bbb"])
+        assert result == [("1", "GPU-bbb", "7.5")]
+
+    def test_dedup_same_device(self):
+        result = select_visible_gpu_rows(self.ROWS, ["0", "0"])
+        assert result == [("0", "GPU-aaa", "8.6")]
+
+    def test_missing_token(self):
+        result = select_visible_gpu_rows(self.ROWS, ["99"])
+        assert result == []
+
+
+# ===========================================================================
+# F. compatible_linux_runtime_lines
+# ===========================================================================
+
+
+class TestCompatibleLinuxRuntimeLines:
+    def test_no_driver(self):
+        host = make_host(driver_cuda_version = None)
+        assert compatible_linux_runtime_lines(host) == []
+
+    def test_driver_11_8(self):
+        host = make_host(driver_cuda_version = (11, 8))
+        assert compatible_linux_runtime_lines(host) == []
+
+    def test_driver_12_4(self):
+        host = make_host(driver_cuda_version = (12, 4))
+        assert compatible_linux_runtime_lines(host) == ["cuda12"]
+
+    def test_driver_13_0(self):
+        host = make_host(driver_cuda_version = (13, 0))
+        assert compatible_linux_runtime_lines(host) == ["cuda13", "cuda12"]
+
+
+# ===========================================================================
+# G. pick_windows_cuda_runtime + compatible_windows_runtime_lines
+# ===========================================================================
+
+
+class TestPickWindowsCudaRuntime:
+    def test_no_driver(self):
+        host = make_host(driver_cuda_version = None)
+        assert pick_windows_cuda_runtime(host) is None
+
+    def test_below_threshold(self):
+        host = make_host(driver_cuda_version = (12, 3))
+        assert pick_windows_cuda_runtime(host) is None
+
+    def test_driver_12_4(self):
+        host = make_host(driver_cuda_version = (12, 4))
+        assert pick_windows_cuda_runtime(host) == "12.4"
+
+    def test_driver_13_1(self):
+        host = make_host(driver_cuda_version = (13, 1))
+        assert pick_windows_cuda_runtime(host) == "13.1"
+
+
+class TestCompatibleWindowsRuntimeLines:
+    def test_no_driver(self):
+        host = make_host(driver_cuda_version = None)
+        assert compatible_windows_runtime_lines(host) == []
+
+    def test_driver_12_4(self):
+        host = make_host(driver_cuda_version = (12, 4))
+        assert compatible_windows_runtime_lines(host) == ["cuda12"]
+
+    def test_driver_13_1(self):
+        host = make_host(driver_cuda_version = (13, 1))
+        assert compatible_windows_runtime_lines(host) == ["cuda13", "cuda12"]
+
+
+# ===========================================================================
+# H. runtime_line_from_cuda_version
+# ===========================================================================
+
+
+class TestRuntimeLineFromCudaVersion:
+    def test_cuda_12(self):
+        assert runtime_line_from_cuda_version("12.6") == "cuda12"
+
+    def test_cuda_13(self):
+        assert runtime_line_from_cuda_version("13.0") == "cuda13"
+
+    def test_cuda_11(self):
+        assert runtime_line_from_cuda_version("11.8") is None
+
+    def test_none(self):
+        assert runtime_line_from_cuda_version(None) is None
+
+    def test_empty(self):
+        assert runtime_line_from_cuda_version("") is None
+
+
+# ===========================================================================
+# I. apply_approved_hashes
+# ===========================================================================
+
+
+class TestApplyApprovedHashes:
+    def _choice(self, name):
+        return AssetChoice(
+            repo = "test",
+            tag = "v1",
+            name = name,
+            url = f"https://x/{name}",
+            source_label = "test",
+        )
+
+    def test_both_approved(self):
+        c1, c2 = self._choice("a.tar.gz"), self._choice("b.tar.gz")
+        checksums = make_checksums(["a.tar.gz", "b.tar.gz"])
+        result = apply_approved_hashes([c1, c2], checksums)
+        assert len(result) == 2
+        assert all(c.expected_sha256 == "a" * 64 for c in result)
+
+    def test_one_approved(self):
+        c1, c2 = self._choice("a.tar.gz"), self._choice("missing.tar.gz")
+        checksums = make_checksums(["a.tar.gz"])
+        result = apply_approved_hashes([c1, c2], checksums)
+        assert len(result) == 1
+        assert result[0].name == "a.tar.gz"
+
+    def test_none_approved(self):
+        c1 = self._choice("missing.tar.gz")
+        checksums = make_checksums(["other.tar.gz"])
+        with pytest.raises(PrebuiltFallback, match = "approved checksum"):
+            apply_approved_hashes([c1], checksums)
+
+    def test_empty_input(self):
+        checksums = make_checksums(["a.tar.gz"])
+        with pytest.raises(PrebuiltFallback, match = "approved checksum"):
+            apply_approved_hashes([], checksums)
+
+
+# ===========================================================================
+# J. linux_cuda_choice_from_release -- core selection
+# ===========================================================================
+
+
+class TestLinuxCudaChoiceFromRelease:
+    # --- Runtime line resolution ---
+
+    def test_no_runtime_lines_detected(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, [])
+        host = make_host(driver_cuda_version = (12, 8))
+        art = make_artifact("bundle-cuda12.tar.gz")
+        release = make_release([art])
+        assert linux_cuda_choice_from_release(host, release) is None
+
+    def test_detected_lines_incompatible_with_driver(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda13"])
+        host = make_host(driver_cuda_version = (12, 4))
+        art = make_artifact("bundle-cuda13.tar.gz", runtime_line = "cuda13")
+        release = make_release([art])
+        assert linux_cuda_choice_from_release(host, release) is None
+
+    def test_driver_13_only_cuda12_detected(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(driver_cuda_version = (13, 0))
+        art = make_artifact("bundle-cuda12.tar.gz", runtime_line = "cuda12")
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+        assert result.primary.runtime_line == "cuda12"
+
+    def test_preferred_runtime_line_reorders(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda13", "cuda12"])
+        host = make_host(driver_cuda_version = (13, 0))
+        art12 = make_artifact("bundle-cuda12.tar.gz", runtime_line = "cuda12")
+        art13 = make_artifact("bundle-cuda13.tar.gz", runtime_line = "cuda13")
+        release = make_release([art12, art13])
+        result = linux_cuda_choice_from_release(
+            host, release, preferred_runtime_line = "cuda12"
+        )
+        assert result is not None
+        assert result.primary.runtime_line == "cuda12"
+
+    def test_preferred_runtime_line_unavailable(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(driver_cuda_version = (12, 8))
+        art = make_artifact("bundle-cuda12.tar.gz", runtime_line = "cuda12")
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(
+            host, release, preferred_runtime_line = "cuda13"
+        )
+        assert result is not None
+        assert result.primary.runtime_line == "cuda12"
+        log_entries = result.selection_log
+        assert any("unavailable_on_host" in entry for entry in log_entries)
+
+    # --- SM matching ---
+
+    def test_exact_sm_match(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact(
+            "bundle.tar.gz", supported_sms = ["75", "86", "89"], min_sm = 75, max_sm = 89
+        )
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+        assert result.primary.name == "bundle.tar.gz"
+
+    def test_sm_not_in_supported_sms(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact(
+            "bundle.tar.gz", supported_sms = ["75", "80", "89"], min_sm = 75, max_sm = 89
+        )
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_sm_outside_min_range(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["50"])
+        art = make_artifact(
+            "bundle.tar.gz", supported_sms = ["50", "75", "86"], min_sm = 75, max_sm = 90
+        )
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_sm_outside_max_range(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["100"])
+        art = make_artifact(
+            "bundle.tar.gz", supported_sms = ["100", "75", "86"], min_sm = 75, max_sm = 90
+        )
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_very_old_sm(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["50"])
+        art = make_artifact("bundle.tar.gz", min_sm = 75, max_sm = 90)
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_very_new_sm(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["100"])
+        art = make_artifact("bundle.tar.gz", min_sm = 75, max_sm = 90)
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    # --- Unknown compute caps (empty list) ---
+
+    def test_unknown_caps_only_portable(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = [])
+        targeted = make_artifact("targeted.tar.gz", coverage_class = "targeted")
+        portable = make_artifact("portable.tar.gz", coverage_class = "portable")
+        release = make_release([targeted, portable])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+        assert result.primary.name == "portable.tar.gz"
+
+    def test_unknown_caps_no_portable(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = [])
+        targeted = make_artifact("targeted.tar.gz", coverage_class = "targeted")
+        release = make_release([targeted])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    # --- Multi-GPU ---
+
+    def test_multi_gpu_all_covered(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["75", "89"])
+        art = make_artifact(
+            "bundle.tar.gz",
+            supported_sms = ["75", "80", "86", "89", "90"],
+            min_sm = 75,
+            max_sm = 90,
+        )
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+
+    def test_multi_gpu_not_all_covered(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["50", "89"])
+        art = make_artifact(
+            "bundle.tar.gz", supported_sms = ["75", "89"], min_sm = 75, max_sm = 89
+        )
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    # --- Artifact selection priority ---
+
+    def test_narrowest_sm_range_wins(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        wide = make_artifact(
+            "wide.tar.gz",
+            supported_sms = ["75", "86", "90"],
+            min_sm = 75,
+            max_sm = 90,
+            rank = 100,
+        )
+        narrow = make_artifact(
+            "narrow.tar.gz",
+            supported_sms = ["80", "86", "89"],
+            min_sm = 80,
+            max_sm = 89,
+            rank = 100,
+        )
+        release = make_release([wide, narrow])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+        assert result.primary.name == "narrow.tar.gz"
+
+    def test_range_tie_lower_rank_wins(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        high = make_artifact(
+            "high.tar.gz",
+            supported_sms = ["75", "86", "90"],
+            min_sm = 75,
+            max_sm = 90,
+            rank = 200,
+        )
+        low = make_artifact(
+            "low.tar.gz",
+            supported_sms = ["75", "86", "90"],
+            min_sm = 75,
+            max_sm = 90,
+            rank = 50,
+        )
+        release = make_release([high, low])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+        assert result.primary.name == "low.tar.gz"
+
+    def test_targeted_preferred_portable_fallback(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        targeted = make_artifact("targeted.tar.gz", coverage_class = "targeted", rank = 100)
+        portable = make_artifact("portable.tar.gz", coverage_class = "portable", rank = 100)
+        release = make_release([targeted, portable])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is not None
+        assert result.primary.name == "targeted.tar.gz"
+        assert len(result.attempts) == 2
+        assert result.attempts[1].name == "portable.tar.gz"
+
+    # --- Edge cases ---
+
+    def test_asset_missing_from_release_assets(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact("bundle.tar.gz")
+        release = make_release([art], assets = {})
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_artifact_empty_supported_sms(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact("bundle.tar.gz", supported_sms = [])
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_artifact_missing_min_sm(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact("bundle.tar.gz", min_sm = None, max_sm = 90)
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_artifact_missing_max_sm(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact("bundle.tar.gz", min_sm = 75, max_sm = None)
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_no_linux_cuda_artifacts(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        art = make_artifact("bundle.tar.gz", install_kind = "windows-cuda")
+        release = make_release([art])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+    def test_empty_artifacts_list(self, monkeypatch):
+        mock_linux_runtime(monkeypatch, ["cuda12"])
+        host = make_host(compute_caps = ["86"])
+        release = make_release([])
+        result = linux_cuda_choice_from_release(host, release)
+        assert result is None
+
+
+# ===========================================================================
+# K. windows_cuda_attempts
+# ===========================================================================
+
+
+class TestWindowsCudaAttempts:
+    TAG = "b8508"
+
+    def _upstream(self, *runtime_versions):
+        assets = {}
+        for rv in runtime_versions:
+            name = f"llama-{self.TAG}-bin-win-cuda-{rv}-x64.zip"
+            assets[name] = f"https://example.com/{name}"
+        return assets
+
+    def test_driver_12_4_no_dlls_fallback(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, [])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4))
+        assets = self._upstream("12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, None)
+        assert len(result) == 1
+        assert result[0].runtime_line == "cuda12"
+
+    def test_driver_13_1_both_dlls(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, ["cuda13", "cuda12"])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (13, 1))
+        assets = self._upstream("13.1", "12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, None)
+        assert len(result) == 2
+        assert result[0].runtime_line == "cuda13"
+        assert result[1].runtime_line == "cuda12"
+
+    def test_preferred_reorders(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, ["cuda13", "cuda12"])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (13, 1))
+        assets = self._upstream("13.1", "12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, "cuda12")
+        assert len(result) == 2
+        assert result[0].runtime_line == "cuda12"
+
+    def test_preferred_unavailable(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, ["cuda12"])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4))
+        assets = self._upstream("12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, "cuda13")
+        assert len(result) == 1
+        assert result[0].runtime_line == "cuda12"
+
+    def test_detected_incompatible_with_driver(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, ["cuda13"])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4))
+        assets = self._upstream("12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, None)
+        assert len(result) == 1
+        assert result[0].runtime_line == "cuda12"
+
+    def test_driver_too_old(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, [])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (11, 8))
+        assets = self._upstream("12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, None)
+        assert result == []
+
+    def test_asset_missing_from_upstream(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, ["cuda12"])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (12, 4))
+        result = windows_cuda_attempts(host, self.TAG, {}, None)
+        assert result == []
+
+    def test_both_assets_present(self, monkeypatch):
+        mock_windows_runtime(monkeypatch, ["cuda13", "cuda12"])
+        host = make_host(system = "Windows", machine = "AMD64", driver_cuda_version = (13, 1))
+        assets = self._upstream("13.1", "12.4")
+        result = windows_cuda_attempts(host, self.TAG, assets, None)
+        assert len(result) == 2
+
+
+# ===========================================================================
+# L. resolve_upstream_asset_choice -- platform routing
+# ===========================================================================
+
+
+class TestResolveUpstreamAssetChoice:
+    TAG = "b8508"
+
+    def _mock_github_assets(self, monkeypatch, assets):
+        monkeypatch.setattr(
+            INSTALL_LLAMA_PREBUILT,
+            "github_release_assets",
+            lambda repo, tag: assets,
+        )
+
+    def test_linux_x86_64_cpu(self, monkeypatch):
+        name = f"llama-{self.TAG}-bin-ubuntu-x64.tar.gz"
+        self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"})
+        host = make_host(
+            has_usable_nvidia = False, nvidia_smi = None, has_physical_nvidia = False
+        )
+        result = resolve_upstream_asset_choice(host, self.TAG)
+        assert result.install_kind == "linux-cpu"
+        assert result.name == name
+
+    def test_linux_cpu_missing(self, monkeypatch):
+        self._mock_github_assets(monkeypatch, {})
+        host = make_host(
+            has_usable_nvidia = False, nvidia_smi = None, has_physical_nvidia = False
+        )
+        with pytest.raises(PrebuiltFallback, match = "Linux CPU"):
+            resolve_upstream_asset_choice(host, self.TAG)
+
+    def test_windows_x86_64_cpu(self, monkeypatch):
+        name = f"llama-{self.TAG}-bin-win-cpu-x64.zip"
+        self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"})
+        host = make_host(
+            system = "Windows",
+            machine = "AMD64",
+            has_usable_nvidia = False,
+            nvidia_smi = None,
+            has_physical_nvidia = False,
+        )
+        result = resolve_upstream_asset_choice(host, self.TAG)
+        assert result.install_kind == "windows-cpu"
+        assert result.name == name
+
+    def test_windows_cpu_missing(self, monkeypatch):
+        self._mock_github_assets(monkeypatch, {})
+        host = make_host(
+            system = "Windows",
+            machine = "AMD64",
+            has_usable_nvidia = False,
+            nvidia_smi = None,
+            has_physical_nvidia = False,
+        )
+        with pytest.raises(PrebuiltFallback, match = "Windows CPU"):
+            resolve_upstream_asset_choice(host, self.TAG)
+
+    def test_macos_arm64(self, monkeypatch):
+        name = f"llama-{self.TAG}-bin-macos-arm64.tar.gz"
+        self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"})
+        host = make_host(
+            system = "Darwin",
+            machine = "arm64",
+            nvidia_smi = None,
+            driver_cuda_version = None,
+            compute_caps = [],
+            has_physical_nvidia = False,
+            has_usable_nvidia = False,
+        )
+        result = resolve_upstream_asset_choice(host, self.TAG)
+        assert result.install_kind == "macos-arm64"
+        assert result.name == name
+
+    def test_macos_arm64_missing(self, monkeypatch):
+        self._mock_github_assets(monkeypatch, {})
+        host = make_host(
+            system = "Darwin",
+            machine = "arm64",
+            nvidia_smi = None,
+            driver_cuda_version = None,
+            compute_caps = [],
+            has_physical_nvidia = False,
+            has_usable_nvidia = False,
+        )
+        with pytest.raises(PrebuiltFallback, match = "macOS arm64"):
+            resolve_upstream_asset_choice(host, self.TAG)
+
+    def test_macos_x86_64(self, monkeypatch):
+        name = f"llama-{self.TAG}-bin-macos-x64.tar.gz"
+        self._mock_github_assets(monkeypatch, {name: f"https://x/{name}"})
+        host = make_host(
+            system = "Darwin",
+            machine = "x86_64",
+            nvidia_smi = None,
+            driver_cuda_version = None,
+            compute_caps = [],
+            has_physical_nvidia = False,
+            has_usable_nvidia = False,
+        )
+        result = resolve_upstream_asset_choice(host, self.TAG)
+        assert result.install_kind == "macos-x64"
+        assert result.name == name
+
+    def test_linux_aarch64(self, monkeypatch):
+        self._mock_github_assets(monkeypatch, {})
+        host = make_host(
+            system = "Linux",
+            machine = "aarch64",
+            nvidia_smi = None,
+            driver_cuda_version = None,
+            compute_caps = [],
+            has_physical_nvidia = False,
+            has_usable_nvidia = False,
+        )
+        with pytest.raises(
+            PrebuiltFallback, match = "no prebuilt policy exists for Linux aarch64"
+        ):
+            resolve_upstream_asset_choice(host, self.TAG)
+
+    def test_windows_usable_nvidia_delegates(self, monkeypatch):
+        cuda_name = f"llama-{self.TAG}-bin-win-cuda-12.4-x64.zip"
+        self._mock_github_assets(monkeypatch, {cuda_name: f"https://x/{cuda_name}"})
+        mock_windows_runtime(monkeypatch, ["cuda12"])
+        monkeypatch.setattr(
+            INSTALL_LLAMA_PREBUILT,
+            "resolve_windows_cuda_choices",
+            lambda host, tag, assets: [
+                AssetChoice(
+                    repo = UPSTREAM_REPO,
+                    tag = tag,
+                    name = cuda_name,
+                    url = f"https://x/{cuda_name}",
+                    source_label = "upstream",
+                    install_kind = "windows-cuda",
+                    runtime_line = "cuda12",
+                )
+            ],
+        )
+        host = make_host(
+            system = "Windows",
+            machine = "AMD64",
+            driver_cuda_version = (12, 4),
+            has_usable_nvidia = True,
+        )
+        result = resolve_upstream_asset_choice(host, self.TAG)
+        assert result.install_kind == "windows-cuda"
+        assert result.name == cuda_name

From d87c21aebf527c57ca14a6f1ab763ce0ec1ce543 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 06:14:33 -0700
Subject: [PATCH 25/34] fix(studio): add -ngl -1 when model fits on GPU to
 enable GPU offloading (#4588)

When _select_gpus determines that a GGUF model fits on the selected
GPU(s), the code sets CUDA_VISIBLE_DEVICES but never passes -ngl
(number of GPU layers) to llama-server. Without -ngl or --fit,
llama-server defaults to 0 GPU layers and runs entirely on CPU.

This adds -ngl -1 (offload all layers) in the elif branch where
gpu_indices is set and use_fit is False, so models that fit in VRAM
actually use the GPU for inference.

Co-authored-by: Daniel Han 
---
 studio/backend/core/inference/llama_cpp.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py
index 7b1db8fd04..81a087341a 100644
--- a/studio/backend/core/inference/llama_cpp.py
+++ b/studio/backend/core/inference/llama_cpp.py
@@ -857,6 +857,9 @@ class LlamaCppBackend:
 
             if use_fit:
                 cmd.extend(["--fit", "on"])
+            elif gpu_indices is not None:
+                # Model fits on selected GPU(s) -- offload all layers
+                cmd.extend(["-ngl", "-1"])
 
             if n_threads is not None:
                 cmd.extend(["--threads", str(n_threads)])

From ae2b1b97ba24b96b82ebab8a55facc5e27645ca2 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 06:24:40 -0700
Subject: [PATCH 26/34] fix(studio): add pip-installed nvidia CUDA libs to
 LD_LIBRARY_PATH for llama-server (#4590)

The prebuilt llama.cpp binary (cuda13-newer) links against
libcudart.so.13 and libcublas.so.13. When torch is installed via pip,
these libraries live in the venv's site-packages under
nvidia/cu13/lib/, not in /usr/local/cuda/.

The existing LD_LIBRARY_PATH logic only searched /usr/local/cuda*
paths (which have CUDA 12.x), so the CUDA backend failed to load
silently and llama-server fell back to CPU -- even with -ngl -1.

This adds a glob scan of the venv's nvidia package directories
(cu*, cudnn, nvjitlink) to LD_LIBRARY_PATH before launching
llama-server, matching where pip puts the CUDA runtime.

Tested on Colab with RTX PRO 6000 Blackwell (CUDA 13.0, pip torch):
before -- 3 MiB GPU, 0% util, CPU inference
after  -- 13317 MiB GPU, 77% util, full GPU inference

Co-authored-by: Daniel Han 
---
 studio/backend/core/inference/llama_cpp.py | 40 ++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py
index 81a087341a..1d5643ac09 100644
--- a/studio/backend/core/inference/llama_cpp.py
+++ b/studio/backend/core/inference/llama_cpp.py
@@ -969,6 +969,46 @@ class LlamaCppBackend:
 
                 lib_dirs = [binary_dir]
                 _arch = platform.machine()  # x86_64, aarch64, etc.
+
+                # Pip-installed nvidia CUDA runtime libs (e.g. torch's
+                # bundled cuda-bindings).  The prebuilt llama.cpp binary
+                # links against libcudart.so.13 / libcublas.so.13 which
+                # live here, not in /usr/local/cuda.
+                import glob as _glob
+
+                for _nv_pattern in [
+                    os.path.join(
+                        sys.prefix,
+                        "lib",
+                        "python*",
+                        "site-packages",
+                        "nvidia",
+                        "cu*",
+                        "lib",
+                    ),
+                    os.path.join(
+                        sys.prefix,
+                        "lib",
+                        "python*",
+                        "site-packages",
+                        "nvidia",
+                        "cudnn",
+                        "lib",
+                    ),
+                    os.path.join(
+                        sys.prefix,
+                        "lib",
+                        "python*",
+                        "site-packages",
+                        "nvidia",
+                        "nvjitlink",
+                        "lib",
+                    ),
+                ]:
+                    for _nv_dir in _glob.glob(_nv_pattern):
+                        if os.path.isdir(_nv_dir):
+                            lib_dirs.append(_nv_dir)
+
                 for cuda_lib in [
                     "/usr/local/cuda/lib64",
                     f"/usr/local/cuda/targets/{_arch}-linux/lib",

From d56b115bb4f27e712f1d78b662962b251fc73c62 Mon Sep 17 00:00:00 2001
From: Roland Tannous 
Date: Wed, 25 Mar 2026 13:24:29 +0000
Subject: [PATCH 27/34] feat: multi-source model discovery (HF default, legacy
 cache, LM Studio)

---
 studio/backend/models/models.py               |   6 +-
 studio/backend/routes/models.py               | 230 +++++++++++++-----
 studio/backend/utils/paths/__init__.py        |   4 +
 studio/backend/utils/paths/storage_roots.py   |  50 +++-
 .../studio/sections/model-section.tsx         |   6 +-
 .../src/features/training/api/models-api.ts   |   3 +-
 6 files changed, 223 insertions(+), 76 deletions(-)

diff --git a/studio/backend/models/models.py b/studio/backend/models/models.py
index daa8eec907..046e36137d 100644
--- a/studio/backend/models/models.py
+++ b/studio/backend/models/models.py
@@ -165,7 +165,7 @@ class LocalModelInfo(BaseModel):
     id: str = Field(..., description = "Identifier to use for loading/training")
     display_name: str = Field(..., description = "Display label")
     path: str = Field(..., description = "Local path where model data was discovered")
-    source: Literal["models_dir", "hf_cache"] = Field(
+    source: Literal["models_dir", "hf_cache", "lmstudio"] = Field(
         ...,
         description = "Discovery source",
     )
@@ -189,6 +189,10 @@ class LocalModelListResponse(BaseModel):
         None,
         description = "HF cache root that was scanned",
     )
+    lmstudio_dirs: List[str] = Field(
+        default_factory = list,
+        description = "LM Studio model directories that were scanned",
+    )
     models: List[LocalModelInfo] = Field(
         default_factory = list,
         description = "Discovered local/cached models",
diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py
index e705762447..63c9304a64 100644
--- a/studio/backend/routes/models.py
+++ b/studio/backend/routes/models.py
@@ -210,6 +210,76 @@ def _scan_hf_cache(cache_dir: Path) -> List[LocalModelInfo]:
     return found
 
 
+def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]:
+    """Scan an LM Studio models directory for model files.
+
+    LM Studio uses a ``publisher/model-name`` folder structure containing
+    GGUF files, or standalone GGUF files at the top level.
+    """
+    if not lm_dir.exists() or not lm_dir.is_dir():
+        return []
+
+    found: List[LocalModelInfo] = []
+    for child in lm_dir.iterdir():
+        if not child.is_dir():
+            if child.suffix == ".gguf" and child.is_file():
+                try:
+                    updated_at = child.stat().st_mtime
+                except OSError:
+                    updated_at = None
+                found.append(
+                    LocalModelInfo(
+                        id = str(child),
+                        display_name = child.stem,
+                        path = str(child),
+                        source = "lmstudio",
+                        updated_at = updated_at,
+                    ),
+                )
+            continue
+
+        # child is a publisher directory — scan its sub-directories
+        for model_dir in child.iterdir():
+            if model_dir.is_dir():
+                has_model = (
+                    any(model_dir.glob("*.gguf"))
+                    or (model_dir / "config.json").exists()
+                    or any(model_dir.glob("*.safetensors"))
+                )
+                if not has_model:
+                    continue
+                model_id = f"{child.name}/{model_dir.name}"
+                try:
+                    updated_at = model_dir.stat().st_mtime
+                except OSError:
+                    updated_at = None
+                found.append(
+                    LocalModelInfo(
+                        id = model_id,
+                        model_id = model_id,
+                        display_name = model_dir.name,
+                        path = str(model_dir),
+                        source = "lmstudio",
+                        updated_at = updated_at,
+                    ),
+                )
+            elif model_dir.suffix == ".gguf" and model_dir.is_file():
+                try:
+                    updated_at = model_dir.stat().st_mtime
+                except OSError:
+                    updated_at = None
+                found.append(
+                    LocalModelInfo(
+                        id = str(model_dir),
+                        display_name = model_dir.stem,
+                        path = str(model_dir),
+                        source = "lmstudio",
+                        updated_at = updated_at,
+                    ),
+                )
+    return found
+
+
 @router.get("/local", response_model = LocalModelListResponse)
 async def list_local_models(
     models_dir: str = Query(
@@ -218,13 +288,24 @@ async def list_local_models(
     current_subject: str = Depends(get_current_subject),
 ):
     """
-    List local model candidates from custom models dir and HF cache.
+    List local model candidates from custom models dir, HF cache,
+    legacy Unsloth HF cache, and LM Studio directories.
     """
+    from utils.paths import legacy_hf_cache_dir, lmstudio_model_dirs
+
+    # Resolve all scan directories up front.
+    hf_cache_dir = _resolve_hf_cache_dir()
+    legacy_hf = legacy_hf_cache_dir()
+    lm_dirs = lmstudio_model_dirs()
+
     # Validate models_dir against an allowlist of trusted directories.
     # Only the trusted Path objects are used for filesystem access -- the
     # user-supplied string is only used for matching, never for path construction.
-    hf_cache_dir = _resolve_hf_cache_dir()
-    allowed_roots = [Path("./models").resolve(), hf_cache_dir]
+    allowed_roots: list[Path] = [Path("./models").resolve(), hf_cache_dir]
+    if legacy_hf.is_dir():
+        allowed_roots.append(legacy_hf)
+    for d in lm_dirs:
+        allowed_roots.append(d)
     try:
         from utils.paths import studio_root, outputs_root
 
@@ -248,6 +329,14 @@ async def list_local_models(
     try:
         local_models = _scan_models_dir(models_root) + _scan_hf_cache(hf_cache_dir)
 
+        # Scan legacy Unsloth HF cache for backward compatibility
+        if legacy_hf.is_dir() and legacy_hf.resolve() != hf_cache_dir.resolve():
+            local_models += _scan_hf_cache(legacy_hf)
+
+        # Scan LM Studio directories
+        for lm_dir in lm_dirs:
+            local_models += _scan_lmstudio_dir(lm_dir)
+
         deduped: dict[str, LocalModelInfo] = {}
         for model in local_models:
             if model.id not in deduped:
@@ -262,6 +351,7 @@ async def list_local_models(
         return LocalModelListResponse(
             models_dir = str(models_root),
             hf_cache_dir = str(hf_cache_dir),
+            lmstudio_dirs = [str(d) for d in lm_dirs],
             models = models,
         )
     except Exception as e:
@@ -850,42 +940,44 @@ def _get_repo_size_cached(repo_id: str) -> int:
 async def list_cached_gguf(
     current_subject: str = Depends(get_current_subject),
 ):
-    """List GGUF repos that have already been downloaded to the HF cache.
-
-    Uses scan_cache_dir() for proper repo IDs, then deduplicates by
-    lowercased key (HF cache dirs are lowercased but the canonical repo
-    ID preserves casing).
-    """
+    """List GGUF repos downloaded to HF cache and legacy Unsloth cache."""
     try:
         from huggingface_hub import scan_cache_dir
+        from utils.paths import legacy_hf_cache_dir
+
+        cache_scans = [scan_cache_dir()]
+        legacy_hf = legacy_hf_cache_dir()
+        if legacy_hf.is_dir():
+            try:
+                cache_scans.append(scan_cache_dir(cache_dir = str(legacy_hf)))
+            except Exception:
+                pass
 
-        hf_cache = scan_cache_dir()
         seen_lower: dict[str, dict] = {}
-        for repo_info in hf_cache.repos:
-            if repo_info.repo_type != "model":
-                continue
-            repo_id = repo_info.repo_id
-            if not repo_id.upper().endswith("-GGUF"):
-                continue
-            # Check for actual .gguf files and sum sizes
-            total_size = 0
-            has_gguf = False
-            for revision in repo_info.revisions:
-                for f in revision.files:
-                    if f.file_name.endswith(".gguf"):
-                        has_gguf = True
-                        total_size += f.size_on_disk
-            if not has_gguf:
-                continue
-            # Deduplicate: keep the entry with the most data
-            key = repo_id.lower()
-            existing = seen_lower.get(key)
-            if existing is None or total_size > existing["size_bytes"]:
-                seen_lower[key] = {
-                    "repo_id": repo_id,
-                    "size_bytes": total_size,
-                    "cache_path": str(repo_info.repo_path),
-                }
+        for hf_cache in cache_scans:
+            for repo_info in hf_cache.repos:
+                if repo_info.repo_type != "model":
+                    continue
+                repo_id = repo_info.repo_id
+                if not repo_id.upper().endswith("-GGUF"):
+                    continue
+                total_size = 0
+                has_gguf = False
+                for revision in repo_info.revisions:
+                    for f in revision.files:
+                        if f.file_name.endswith(".gguf"):
+                            has_gguf = True
+                            total_size += f.size_on_disk
+                if not has_gguf:
+                    continue
+                key = repo_id.lower()
+                existing = seen_lower.get(key)
+                if existing is None or total_size > existing["size_bytes"]:
+                    seen_lower[key] = {
+                        "repo_id": repo_id,
+                        "size_bytes": total_size,
+                        "cache_path": str(repo_info.repo_path),
+                    }
         cached = sorted(seen_lower.values(), key = lambda c: c["repo_id"])
         return {"cached": cached}
     except Exception as e:
@@ -897,44 +989,48 @@ async def list_cached_gguf(
 async def list_cached_models(
     current_subject: str = Depends(get_current_subject),
 ):
-    """List non-GGUF model repos that have been downloaded to the HF cache.
-
-    Only includes repos that actually contain model weight files
-    (.safetensors, .bin), not repos with only config/metadata.
-    """
+    """List non-GGUF model repos downloaded to HF cache and legacy Unsloth cache."""
     _WEIGHT_EXTENSIONS = (".safetensors", ".bin")
 
     try:
         from huggingface_hub import scan_cache_dir
+        from utils.paths import legacy_hf_cache_dir
+
+        cache_scans = [scan_cache_dir()]
+        legacy_hf = legacy_hf_cache_dir()
+        if legacy_hf.is_dir():
+            try:
+                cache_scans.append(scan_cache_dir(cache_dir = str(legacy_hf)))
+            except Exception:
+                pass
 
-        hf_cache = scan_cache_dir()
         seen_lower: dict[str, dict] = {}
-        for repo_info in hf_cache.repos:
-            if repo_info.repo_type != "model":
-                continue
-            repo_id = repo_info.repo_id
-            if repo_id.upper().endswith("-GGUF"):
-                continue
-            total_size = sum(
-                f.size_on_disk for rev in repo_info.revisions for f in rev.files
-            )
-            if total_size == 0:
-                continue
-            # Skip repos that only have config/metadata files (no weights)
-            has_weights = any(
-                f.file_name.endswith(_WEIGHT_EXTENSIONS)
-                for rev in repo_info.revisions
-                for f in rev.files
-            )
-            if not has_weights:
-                continue
-            key = repo_id.lower()
-            existing = seen_lower.get(key)
-            if existing is None or total_size > existing["size_bytes"]:
-                seen_lower[key] = {
-                    "repo_id": repo_id,
-                    "size_bytes": total_size,
-                }
+        for hf_cache in cache_scans:
+            for repo_info in hf_cache.repos:
+                if repo_info.repo_type != "model":
+                    continue
+                repo_id = repo_info.repo_id
+                if repo_id.upper().endswith("-GGUF"):
+                    continue
+                total_size = sum(
+                    f.size_on_disk for rev in repo_info.revisions for f in rev.files
+                )
+                if total_size == 0:
+                    continue
+                has_weights = any(
+                    f.file_name.endswith(_WEIGHT_EXTENSIONS)
+                    for rev in repo_info.revisions
+                    for f in rev.files
+                )
+                if not has_weights:
+                    continue
+                key = repo_id.lower()
+                existing = seen_lower.get(key)
+                if existing is None or total_size > existing["size_bytes"]:
+                    seen_lower[key] = {
+                        "repo_id": repo_id,
+                        "size_bytes": total_size,
+                    }
         cached = sorted(seen_lower.values(), key = lambda c: c["repo_id"])
         return {"cached": cached}
     except Exception as e:
diff --git a/studio/backend/utils/paths/__init__.py b/studio/backend/utils/paths/__init__.py
index 789052f372..aec6bb1292 100644
--- a/studio/backend/utils/paths/__init__.py
+++ b/studio/backend/utils/paths/__init__.py
@@ -23,6 +23,8 @@ from .storage_roots import (
     unstructured_uploads_root,
     oxc_validator_tmp_root,
     tensorboard_root,
+    legacy_hf_cache_dir,
+    lmstudio_model_dirs,
     ensure_dir,
     ensure_studio_directories,
     resolve_under_root,
@@ -53,6 +55,8 @@ __all__ = [
     "unstructured_uploads_root",
     "oxc_validator_tmp_root",
     "tensorboard_root",
+    "legacy_hf_cache_dir",
+    "lmstudio_model_dirs",
     "ensure_dir",
     "ensure_studio_directories",
     "resolve_under_root",
diff --git a/studio/backend/utils/paths/storage_roots.py b/studio/backend/utils/paths/storage_roots.py
index 626e868275..08d3744e95 100644
--- a/studio/backend/utils/paths/storage_roots.py
+++ b/studio/backend/utils/paths/storage_roots.py
@@ -3,7 +3,9 @@
 
 from __future__ import annotations
 
+import json
 import os
+import sys
 from pathlib import Path
 import tempfile
 
@@ -82,19 +84,55 @@ def ensure_dir(path: Path) -> Path:
     return path
 
 
+def legacy_hf_cache_dir() -> Path:
+    """Old Unsloth-specific HF hub cache, kept for backward-compat scanning."""
+    return cache_root() / "huggingface" / "hub"
+
+
+def lmstudio_model_dirs() -> list[Path]:
+    """Return LM Studio model directories that exist on disk."""
+    dirs: list[Path] = []
+
+    # 1. Check LM Studio settings.json for custom downloads folder
+    settings_path = Path.home() / ".lmstudio" / "settings.json"
+    if settings_path.is_file():
+        try:
+            with open(settings_path) as f:
+                settings = json.load(f)
+            downloads = settings.get("downloadsFolder", "")
+            if downloads:
+                p = Path(downloads).expanduser()
+                if p.is_dir():
+                    dirs.append(p)
+        except Exception:
+            pass
+
+    # 2. Legacy LM Studio cache (Linux/macOS)
+    if sys.platform == "win32":
+        legacy = Path.home() / ".cache" / "lm-studio" / "models"
+    else:
+        legacy = Path.home() / ".cache" / "lm-studio" / "models"
+    if legacy.is_dir():
+        dirs.append(legacy)
+
+    return dirs
+
+
 def _setup_cache_env() -> None:
-    """Set cache environment variables for HuggingFace, uv, and vLLM.
+    """Set cache environment variables for uv and vLLM.
+
+    HuggingFace cache variables (HF_HOME, HF_HUB_CACHE, HF_XET_CACHE)
+    are no longer overridden — HF uses its own defaults unless the user
+    has explicitly set them.  The legacy Unsloth HF cache at
+    ``~/.unsloth/studio/cache/huggingface/hub`` is still scanned for
+    backward compatibility via :func:`legacy_hf_cache_dir`.
 
     Only sets variables that are not already set by the user, so
-    explicit overrides (e.g. HF_HOME=/data/hf) are respected.
+    explicit overrides are respected.
     Works on Linux, macOS, and Windows.
     """
     root = cache_root()
-    hf_dir = root / "huggingface"
     defaults = {
-        "HF_HOME": str(hf_dir),
-        "HF_HUB_CACHE": str(hf_dir / "hub"),
-        "HF_XET_CACHE": str(hf_dir / "xet"),
         "UV_CACHE_DIR": str(root / "uv"),
         "VLLM_CACHE_ROOT": str(root / "vllm"),
     }
diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx
index e9bcde17b5..84df506769 100644
--- a/studio/frontend/src/features/studio/sections/model-section.tsx
+++ b/studio/frontend/src/features/studio/sections/model-section.tsx
@@ -334,7 +334,11 @@ export function ModelSection() {
                   {(id: string) => {
                     const model = localMetaById.get(id);
                     const source =
-                      model?.source === "hf_cache" ? "HF cache" : "Local dir";
+                      model?.source === "hf_cache"
+                        ? "HF cache"
+                        : model?.source === "lmstudio"
+                          ? "LM Studio"
+                          : "Local dir";
                     return (
                       
                         
diff --git a/studio/frontend/src/features/training/api/models-api.ts b/studio/frontend/src/features/training/api/models-api.ts
index 84051e3e1d..f2b1e256c2 100644
--- a/studio/frontend/src/features/training/api/models-api.ts
+++ b/studio/frontend/src/features/training/api/models-api.ts
@@ -79,7 +79,7 @@ export interface LocalModelInfo {
   id: string;
   display_name: string;
   path: string;
-  source: "models_dir" | "hf_cache";
+  source: "models_dir" | "hf_cache" | "lmstudio";
   model_id?: string | null;
   updated_at?: number | null;
 }
@@ -87,6 +87,7 @@ export interface LocalModelInfo {
 interface LocalModelListResponse {
   models_dir: string;
   hf_cache_dir?: string | null;
+  lmstudio_dirs?: string[];
   models: LocalModelInfo[];
 }
 

From 1f498a73e6b0a7c9cb83d791f869623ebf9429f2 Mon Sep 17 00:00:00 2001
From: Roland Tannous 
Date: Wed, 25 Mar 2026 13:35:03 +0000
Subject: [PATCH 28/34] Revert "feat: multi-source model discovery (HF default,
 legacy cache, LM Studio)"

This reverts commit d56b115bb4f27e712f1d78b662962b251fc73c62.
---
 studio/backend/models/models.py               |   6 +-
 studio/backend/routes/models.py               | 230 +++++-------------
 studio/backend/utils/paths/__init__.py        |   4 -
 studio/backend/utils/paths/storage_roots.py   |  50 +---
 .../studio/sections/model-section.tsx         |   6 +-
 .../src/features/training/api/models-api.ts   |   3 +-
 6 files changed, 76 insertions(+), 223 deletions(-)

diff --git a/studio/backend/models/models.py b/studio/backend/models/models.py
index 046e36137d..daa8eec907 100644
--- a/studio/backend/models/models.py
+++ b/studio/backend/models/models.py
@@ -165,7 +165,7 @@ class LocalModelInfo(BaseModel):
     id: str = Field(..., description = "Identifier to use for loading/training")
     display_name: str = Field(..., description = "Display label")
     path: str = Field(..., description = "Local path where model data was discovered")
-    source: Literal["models_dir", "hf_cache", "lmstudio"] = Field(
+    source: Literal["models_dir", "hf_cache"] = Field(
         ...,
         description = "Discovery source",
     )
@@ -189,10 +189,6 @@ class LocalModelListResponse(BaseModel):
         None,
         description = "HF cache root that was scanned",
     )
-    lmstudio_dirs: List[str] = Field(
-        default_factory = list,
-        description = "LM Studio model directories that were scanned",
-    )
     models: List[LocalModelInfo] = Field(
         default_factory = list,
         description = "Discovered local/cached models",
diff --git a/studio/backend/routes/models.py b/studio/backend/routes/models.py
index 63c9304a64..e705762447 100644
--- a/studio/backend/routes/models.py
+++ b/studio/backend/routes/models.py
@@ -210,76 +210,6 @@ def _scan_hf_cache(cache_dir: Path) -> List[LocalModelInfo]:
     return found
 
 
-def _scan_lmstudio_dir(lm_dir: Path) -> List[LocalModelInfo]:
-    """Scan an LM Studio models directory for model files.
-
-    LM Studio uses a ``publisher/model-name`` folder structure containing
-    GGUF files, or standalone GGUF files at the top level.
-    """
-    if not lm_dir.exists() or not lm_dir.is_dir():
-        return []
-
-    found: List[LocalModelInfo] = []
-    for child in lm_dir.iterdir():
-        if not child.is_dir():
-            if child.suffix == ".gguf" and child.is_file():
-                try:
-                    updated_at = child.stat().st_mtime
-                except OSError:
-                    updated_at = None
-                found.append(
-                    LocalModelInfo(
-                        id = str(child),
-                        display_name = child.stem,
-                        path = str(child),
-                        source = "lmstudio",
-                        updated_at = updated_at,
-                    ),
-                )
-            continue
-
-        # child is a publisher directory — scan its sub-directories
-        for model_dir in child.iterdir():
-            if model_dir.is_dir():
-                has_model = (
-                    any(model_dir.glob("*.gguf"))
-                    or (model_dir / "config.json").exists()
-                    or any(model_dir.glob("*.safetensors"))
-                )
-                if not has_model:
-                    continue
-                model_id = f"{child.name}/{model_dir.name}"
-                try:
-                    updated_at = model_dir.stat().st_mtime
-                except OSError:
-                    updated_at = None
-                found.append(
-                    LocalModelInfo(
-                        id = model_id,
-                        model_id = model_id,
-                        display_name = model_dir.name,
-                        path = str(model_dir),
-                        source = "lmstudio",
-                        updated_at = updated_at,
-                    ),
-                )
-            elif model_dir.suffix == ".gguf" and model_dir.is_file():
-                try:
-                    updated_at = model_dir.stat().st_mtime
-                except OSError:
-                    updated_at = None
-                found.append(
-                    LocalModelInfo(
-                        id = str(model_dir),
-                        display_name = model_dir.stem,
-                        path = str(model_dir),
-                        source = "lmstudio",
-                        updated_at = updated_at,
-                    ),
-                )
-    return found
-
-
 @router.get("/local", response_model = LocalModelListResponse)
 async def list_local_models(
     models_dir: str = Query(
@@ -288,24 +218,13 @@ async def list_local_models(
     current_subject: str = Depends(get_current_subject),
 ):
     """
-    List local model candidates from custom models dir, HF cache,
-    legacy Unsloth HF cache, and LM Studio directories.
+    List local model candidates from custom models dir and HF cache.
     """
-    from utils.paths import legacy_hf_cache_dir, lmstudio_model_dirs
-
-    # Resolve all scan directories up front.
-    hf_cache_dir = _resolve_hf_cache_dir()
-    legacy_hf = legacy_hf_cache_dir()
-    lm_dirs = lmstudio_model_dirs()
-
     # Validate models_dir against an allowlist of trusted directories.
     # Only the trusted Path objects are used for filesystem access -- the
     # user-supplied string is only used for matching, never for path construction.
-    allowed_roots: list[Path] = [Path("./models").resolve(), hf_cache_dir]
-    if legacy_hf.is_dir():
-        allowed_roots.append(legacy_hf)
-    for d in lm_dirs:
-        allowed_roots.append(d)
+    hf_cache_dir = _resolve_hf_cache_dir()
+    allowed_roots = [Path("./models").resolve(), hf_cache_dir]
     try:
         from utils.paths import studio_root, outputs_root
 
@@ -329,14 +248,6 @@ async def list_local_models(
     try:
         local_models = _scan_models_dir(models_root) + _scan_hf_cache(hf_cache_dir)
 
-        # Scan legacy Unsloth HF cache for backward compatibility
-        if legacy_hf.is_dir() and legacy_hf.resolve() != hf_cache_dir.resolve():
-            local_models += _scan_hf_cache(legacy_hf)
-
-        # Scan LM Studio directories
-        for lm_dir in lm_dirs:
-            local_models += _scan_lmstudio_dir(lm_dir)
-
         deduped: dict[str, LocalModelInfo] = {}
         for model in local_models:
             if model.id not in deduped:
@@ -351,7 +262,6 @@ async def list_local_models(
         return LocalModelListResponse(
             models_dir = str(models_root),
             hf_cache_dir = str(hf_cache_dir),
-            lmstudio_dirs = [str(d) for d in lm_dirs],
             models = models,
         )
     except Exception as e:
@@ -940,44 +850,42 @@ def _get_repo_size_cached(repo_id: str) -> int:
 async def list_cached_gguf(
     current_subject: str = Depends(get_current_subject),
 ):
-    """List GGUF repos downloaded to HF cache and legacy Unsloth cache."""
+    """List GGUF repos that have already been downloaded to the HF cache.
+
+    Uses scan_cache_dir() for proper repo IDs, then deduplicates by
+    lowercased key (HF cache dirs are lowercased but the canonical repo
+    ID preserves casing).
+    """
     try:
         from huggingface_hub import scan_cache_dir
-        from utils.paths import legacy_hf_cache_dir
-
-        cache_scans = [scan_cache_dir()]
-        legacy_hf = legacy_hf_cache_dir()
-        if legacy_hf.is_dir():
-            try:
-                cache_scans.append(scan_cache_dir(cache_dir = str(legacy_hf)))
-            except Exception:
-                pass
 
+        hf_cache = scan_cache_dir()
         seen_lower: dict[str, dict] = {}
-        for hf_cache in cache_scans:
-            for repo_info in hf_cache.repos:
-                if repo_info.repo_type != "model":
-                    continue
-                repo_id = repo_info.repo_id
-                if not repo_id.upper().endswith("-GGUF"):
-                    continue
-                total_size = 0
-                has_gguf = False
-                for revision in repo_info.revisions:
-                    for f in revision.files:
-                        if f.file_name.endswith(".gguf"):
-                            has_gguf = True
-                            total_size += f.size_on_disk
-                if not has_gguf:
-                    continue
-                key = repo_id.lower()
-                existing = seen_lower.get(key)
-                if existing is None or total_size > existing["size_bytes"]:
-                    seen_lower[key] = {
-                        "repo_id": repo_id,
-                        "size_bytes": total_size,
-                        "cache_path": str(repo_info.repo_path),
-                    }
+        for repo_info in hf_cache.repos:
+            if repo_info.repo_type != "model":
+                continue
+            repo_id = repo_info.repo_id
+            if not repo_id.upper().endswith("-GGUF"):
+                continue
+            # Check for actual .gguf files and sum sizes
+            total_size = 0
+            has_gguf = False
+            for revision in repo_info.revisions:
+                for f in revision.files:
+                    if f.file_name.endswith(".gguf"):
+                        has_gguf = True
+                        total_size += f.size_on_disk
+            if not has_gguf:
+                continue
+            # Deduplicate: keep the entry with the most data
+            key = repo_id.lower()
+            existing = seen_lower.get(key)
+            if existing is None or total_size > existing["size_bytes"]:
+                seen_lower[key] = {
+                    "repo_id": repo_id,
+                    "size_bytes": total_size,
+                    "cache_path": str(repo_info.repo_path),
+                }
         cached = sorted(seen_lower.values(), key = lambda c: c["repo_id"])
         return {"cached": cached}
     except Exception as e:
@@ -989,48 +897,44 @@ async def list_cached_gguf(
 async def list_cached_models(
     current_subject: str = Depends(get_current_subject),
 ):
-    """List non-GGUF model repos downloaded to HF cache and legacy Unsloth cache."""
+    """List non-GGUF model repos that have been downloaded to the HF cache.
+
+    Only includes repos that actually contain model weight files
+    (.safetensors, .bin), not repos with only config/metadata.
+    """
     _WEIGHT_EXTENSIONS = (".safetensors", ".bin")
 
     try:
         from huggingface_hub import scan_cache_dir
-        from utils.paths import legacy_hf_cache_dir
-
-        cache_scans = [scan_cache_dir()]
-        legacy_hf = legacy_hf_cache_dir()
-        if legacy_hf.is_dir():
-            try:
-                cache_scans.append(scan_cache_dir(cache_dir = str(legacy_hf)))
-            except Exception:
-                pass
 
+        hf_cache = scan_cache_dir()
         seen_lower: dict[str, dict] = {}
-        for hf_cache in cache_scans:
-            for repo_info in hf_cache.repos:
-                if repo_info.repo_type != "model":
-                    continue
-                repo_id = repo_info.repo_id
-                if repo_id.upper().endswith("-GGUF"):
-                    continue
-                total_size = sum(
-                    f.size_on_disk for rev in repo_info.revisions for f in rev.files
-                )
-                if total_size == 0:
-                    continue
-                has_weights = any(
-                    f.file_name.endswith(_WEIGHT_EXTENSIONS)
-                    for rev in repo_info.revisions
-                    for f in rev.files
-                )
-                if not has_weights:
-                    continue
-                key = repo_id.lower()
-                existing = seen_lower.get(key)
-                if existing is None or total_size > existing["size_bytes"]:
-                    seen_lower[key] = {
-                        "repo_id": repo_id,
-                        "size_bytes": total_size,
-                    }
+        for repo_info in hf_cache.repos:
+            if repo_info.repo_type != "model":
+                continue
+            repo_id = repo_info.repo_id
+            if repo_id.upper().endswith("-GGUF"):
+                continue
+            total_size = sum(
+                f.size_on_disk for rev in repo_info.revisions for f in rev.files
+            )
+            if total_size == 0:
+                continue
+            # Skip repos that only have config/metadata files (no weights)
+            has_weights = any(
+                f.file_name.endswith(_WEIGHT_EXTENSIONS)
+                for rev in repo_info.revisions
+                for f in rev.files
+            )
+            if not has_weights:
+                continue
+            key = repo_id.lower()
+            existing = seen_lower.get(key)
+            if existing is None or total_size > existing["size_bytes"]:
+                seen_lower[key] = {
+                    "repo_id": repo_id,
+                    "size_bytes": total_size,
+                }
         cached = sorted(seen_lower.values(), key = lambda c: c["repo_id"])
         return {"cached": cached}
     except Exception as e:
diff --git a/studio/backend/utils/paths/__init__.py b/studio/backend/utils/paths/__init__.py
index aec6bb1292..789052f372 100644
--- a/studio/backend/utils/paths/__init__.py
+++ b/studio/backend/utils/paths/__init__.py
@@ -23,8 +23,6 @@ from .storage_roots import (
     unstructured_uploads_root,
     oxc_validator_tmp_root,
     tensorboard_root,
-    legacy_hf_cache_dir,
-    lmstudio_model_dirs,
     ensure_dir,
     ensure_studio_directories,
     resolve_under_root,
@@ -55,8 +53,6 @@ __all__ = [
     "unstructured_uploads_root",
     "oxc_validator_tmp_root",
     "tensorboard_root",
-    "legacy_hf_cache_dir",
-    "lmstudio_model_dirs",
     "ensure_dir",
     "ensure_studio_directories",
     "resolve_under_root",
diff --git a/studio/backend/utils/paths/storage_roots.py b/studio/backend/utils/paths/storage_roots.py
index 08d3744e95..626e868275 100644
--- a/studio/backend/utils/paths/storage_roots.py
+++ b/studio/backend/utils/paths/storage_roots.py
@@ -3,9 +3,7 @@
 
 from __future__ import annotations
 
-import json
 import os
-import sys
 from pathlib import Path
 import tempfile
 
@@ -84,55 +82,19 @@ def ensure_dir(path: Path) -> Path:
     return path
 
 
-def legacy_hf_cache_dir() -> Path:
-    """Old Unsloth-specific HF hub cache, kept for backward-compat scanning."""
-    return cache_root() / "huggingface" / "hub"
-
-
-def lmstudio_model_dirs() -> list[Path]:
-    """Return LM Studio model directories that exist on disk."""
-    dirs: list[Path] = []
-
-    # 1. Check LM Studio settings.json for custom downloads folder
-    settings_path = Path.home() / ".lmstudio" / "settings.json"
-    if settings_path.is_file():
-        try:
-            with open(settings_path) as f:
-                settings = json.load(f)
-            downloads = settings.get("downloadsFolder", "")
-            if downloads:
-                p = Path(downloads).expanduser()
-                if p.is_dir():
-                    dirs.append(p)
-        except Exception:
-            pass
-
-    # 2. Legacy LM Studio cache (Linux/macOS)
-    if sys.platform == "win32":
-        legacy = Path.home() / ".cache" / "lm-studio" / "models"
-    else:
-        legacy = Path.home() / ".cache" / "lm-studio" / "models"
-    if legacy.is_dir():
-        dirs.append(legacy)
-
-    return dirs
-
-
 def _setup_cache_env() -> None:
-    """Set cache environment variables for uv and vLLM.
-
-    HuggingFace cache variables (HF_HOME, HF_HUB_CACHE, HF_XET_CACHE)
-    are no longer overridden — HF uses its own defaults unless the user
-    has explicitly set them.  The legacy Unsloth HF cache at
-    ``~/.unsloth/studio/cache/huggingface/hub`` is still scanned for
-    backward compatibility via :func:`legacy_hf_cache_dir`.
+    """Set cache environment variables for HuggingFace, uv, and vLLM.
 
     Only sets variables that are not already set by the user, so
-    explicit overrides are respected.
+    explicit overrides (e.g. HF_HOME=/data/hf) are respected.
     Works on Linux, macOS, and Windows.
     """
     root = cache_root()
+    hf_dir = root / "huggingface"
     defaults = {
+        "HF_HOME": str(hf_dir),
+        "HF_HUB_CACHE": str(hf_dir / "hub"),
+        "HF_XET_CACHE": str(hf_dir / "xet"),
         "UV_CACHE_DIR": str(root / "uv"),
         "VLLM_CACHE_ROOT": str(root / "vllm"),
     }
diff --git a/studio/frontend/src/features/studio/sections/model-section.tsx b/studio/frontend/src/features/studio/sections/model-section.tsx
index 84df506769..e9bcde17b5 100644
--- a/studio/frontend/src/features/studio/sections/model-section.tsx
+++ b/studio/frontend/src/features/studio/sections/model-section.tsx
@@ -334,11 +334,7 @@ export function ModelSection() {
                   {(id: string) => {
                     const model = localMetaById.get(id);
                     const source =
-                      model?.source === "hf_cache"
-                        ? "HF cache"
-                        : model?.source === "lmstudio"
-                          ? "LM Studio"
-                          : "Local dir";
+                      model?.source === "hf_cache" ? "HF cache" : "Local dir";
                     return (
                       
                         
diff --git a/studio/frontend/src/features/training/api/models-api.ts b/studio/frontend/src/features/training/api/models-api.ts
index f2b1e256c2..84051e3e1d 100644
--- a/studio/frontend/src/features/training/api/models-api.ts
+++ b/studio/frontend/src/features/training/api/models-api.ts
@@ -79,7 +79,7 @@ export interface LocalModelInfo {
   id: string;
   display_name: string;
   path: string;
-  source: "models_dir" | "hf_cache" | "lmstudio";
+  source: "models_dir" | "hf_cache";
   model_id?: string | null;
   updated_at?: number | null;
 }
@@ -87,7 +87,6 @@ export interface LocalModelInfo {
 interface LocalModelListResponse {
   models_dir: string;
   hf_cache_dir?: string | null;
-  lmstudio_dirs?: string[];
   models: LocalModelInfo[];
 }
 

From 457c42964fd4f8d88fd9960cb2e2e7d1ad87747f Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 06:38:32 -0700
Subject: [PATCH 29/34] fix(studio): validate bun install and retry from
 official source on failure (#4589)

bun install (specifically the npm "bun" shim v1.3.x installed via
npm install -g bun) can exit 0 while silently failing to install
packages. This causes the frontend build to fail with "tsc: not found"
or missing type declarations, since the fallback to npm only triggers
on a non-zero exit code.

Changes:

1. Initial bun install now tries the official bun.sh installer first
   (which gives a real bun runtime), falling back to npm install -g bun
   only if that fails.

2. After bun install reports success, verify that critical binaries
   (tsc, vite) actually exist in node_modules/.bin/. If they are
   missing, reinstall bun from the official source and retry once
   before falling back to npm.

3. Extract the bun install + validation logic into _try_bun_install()
   to avoid duplicating the check/cleanup across both attempts.
---
 studio/setup.sh | 77 +++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 61 insertions(+), 16 deletions(-)

diff --git a/studio/setup.sh b/studio/setup.sh
index 4cfabec95e..90631f6131 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -164,17 +164,26 @@ fi
 echo "✅ Node $(node -v) | npm $(npm -v)"
 
 # ── Install bun (optional, faster package installs) ──
-# Uses npm to install bun globally — Node is already guaranteed above,
-# avoids platform-specific installers, PATH issues, and admin requirements.
+# Try the official bun installer first (gives a real bun runtime).
+# Fall back to npm install -g bun (gives a shim that may be outdated).
+# If neither works, bun is simply skipped and npm handles everything.
 if ! command -v bun &>/dev/null; then
     echo "   Installing bun (faster frontend package installs)..."
-    if npm install -g bun > /dev/null 2>&1 && command -v bun &>/dev/null; then
-        echo "✅ bun installed ($(bun --version))"
+    if curl -fsSL https://bun.sh/install 2>/dev/null | bash > /dev/null 2>&1; then
+        export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
+        export PATH="$BUN_INSTALL/bin:$PATH"
+    fi
+    if ! command -v bun &>/dev/null; then
+        # Official installer failed or unavailable, try npm shim
+        npm install -g bun > /dev/null 2>&1 || true
+    fi
+    if command -v bun &>/dev/null; then
+        echo "   bun installed ($(bun --version))"
     else
         echo "   bun install skipped (npm will be used instead)"
     fi
 else
-    echo "✅ bun already installed ($(bun --version))"
+    echo "   bun already installed ($(bun --version))"
 fi
 
 # ── 5. Build frontend ──
@@ -202,24 +211,60 @@ _restore_gitignores() {
 trap _restore_gitignores EXIT
 
 # Use bun for install if available (faster), fall back to npm.
-# Build always uses npm (Node runtime — avoids bun runtime issues on some platforms).
+# Build always uses npm (Node runtime -- avoids bun runtime issues on some platforms).
 # NOTE: We intentionally avoid run_quiet for the bun install attempt because
 # run_quiet calls exit on failure, which would kill the script before the npm
 # fallback can run. Instead we capture output manually and only show it on failure.
+#
+# IMPORTANT: bun install can exit 0 but silently fail to install packages.
+# The npm "bun" shim (v1.3.x) is known to do this. After bun install reports
+# success, we verify that critical binaries (tsc, vite) actually landed in
+# node_modules/.bin/. If they are missing we reinstall bun from the official
+# source and retry once before falling back to npm.
+_try_bun_install() {
+    local _log _exit_code=0
+    _log=$(mktemp)
+    bun install >"$_log" 2>&1 || _exit_code=$?
+
+    if [ "$_exit_code" -eq 0 ] && [ -x node_modules/.bin/tsc ] && [ -x node_modules/.bin/vite ]; then
+        rm -f "$_log"
+        return 0
+    fi
+
+    # Either bun install failed or it exited 0 but left packages missing
+    if [ "$_exit_code" -ne 0 ]; then
+        echo "   bun install failed (exit code $_exit_code):"
+    else
+        echo "   bun install exited 0 but critical binaries are missing:"
+    fi
+    sed 's/^/   | /' "$_log" >&2
+    rm -f "$_log"
+    rm -rf node_modules
+    return 1
+}
+
+_bun_install_ok=false
 if command -v bun &>/dev/null; then
     echo "   Using bun for package install (faster)"
-    _bun_log=$(mktemp)
-    if bun install >"$_bun_log" 2>&1; then
-        rm -f "$_bun_log"
+    if _try_bun_install; then
+        _bun_install_ok=true
     else
-        echo "   ⚠️  bun install failed, falling back to npm"
-        echo "   bun install output:"
-        sed 's/^/   | /' "$_bun_log" >&2
-        rm -f "$_bun_log"
-        rm -rf node_modules
-        run_quiet "npm install" npm install
+        # First attempt failed -- try reinstalling bun from official source and retry
+        echo "   Reinstalling bun from bun.sh and retrying..."
+        if curl -fsSL https://bun.sh/install 2>/dev/null | bash > /dev/null 2>&1; then
+            export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
+            export PATH="$BUN_INSTALL/bin:$PATH"
+            hash -r 2>/dev/null || true
+        fi
+        if command -v bun &>/dev/null; then
+            echo "   bun reinstalled ($(bun --version)), retrying..."
+            if _try_bun_install; then
+                _bun_install_ok=true
+            fi
+        fi
     fi
-else
+fi
+if [ "$_bun_install_ok" = false ]; then
     run_quiet "npm install" npm install
 fi
 run_quiet "npm run build" npm run build

From 2e4569e06a533d0d1a29774ffa7e9fa161df419a Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 07:05:02 -0700
Subject: [PATCH 30/34] fix(studio): clear bun cache on failure and retry
 before falling back to npm (#4594)

bun's package cache can become corrupt, storing only package metadata
(package.json, README) without actual content (bin/, lib/). When this
happens, bun install exits 0 and reports packages as installed, but
binaries like tsc are missing from node_modules/.bin/.

For example, a corrupt typescript cache entry is 64KB (metadata only)
vs 23MB when correctly downloaded.

Changes:
- After bun install, verify tsc and vite exist in node_modules/.bin/
- If missing, clear the bun cache with bun pm cache rm and retry once
- Only fall back to npm if the retry also fails
- Revert bun installation to npm install -g bun (the binary is fine,
  the cache was the problem)
---
 studio/setup.sh | 43 ++++++++++++++-----------------------------
 1 file changed, 14 insertions(+), 29 deletions(-)

diff --git a/studio/setup.sh b/studio/setup.sh
index 90631f6131..c095fc7245 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -164,20 +164,11 @@ fi
 echo "✅ Node $(node -v) | npm $(npm -v)"
 
 # ── Install bun (optional, faster package installs) ──
-# Try the official bun installer first (gives a real bun runtime).
-# Fall back to npm install -g bun (gives a shim that may be outdated).
-# If neither works, bun is simply skipped and npm handles everything.
+# Uses npm to install bun globally -- Node is already guaranteed above,
+# avoids platform-specific installers, PATH issues, and admin requirements.
 if ! command -v bun &>/dev/null; then
     echo "   Installing bun (faster frontend package installs)..."
-    if curl -fsSL https://bun.sh/install 2>/dev/null | bash > /dev/null 2>&1; then
-        export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
-        export PATH="$BUN_INSTALL/bin:$PATH"
-    fi
-    if ! command -v bun &>/dev/null; then
-        # Official installer failed or unavailable, try npm shim
-        npm install -g bun > /dev/null 2>&1 || true
-    fi
-    if command -v bun &>/dev/null; then
+    if npm install -g bun > /dev/null 2>&1 && command -v bun &>/dev/null; then
         echo "   bun installed ($(bun --version))"
     else
         echo "   bun install skipped (npm will be used instead)"
@@ -216,11 +207,11 @@ trap _restore_gitignores EXIT
 # run_quiet calls exit on failure, which would kill the script before the npm
 # fallback can run. Instead we capture output manually and only show it on failure.
 #
-# IMPORTANT: bun install can exit 0 but silently fail to install packages.
-# The npm "bun" shim (v1.3.x) is known to do this. After bun install reports
-# success, we verify that critical binaries (tsc, vite) actually landed in
-# node_modules/.bin/. If they are missing we reinstall bun from the official
-# source and retry once before falling back to npm.
+# IMPORTANT: bun's package cache can become corrupt -- packages get stored
+# with only metadata (package.json, README) but no actual content (bin/,
+# lib/). When this happens bun install exits 0 but leaves binaries missing.
+# We verify critical binaries after install. If missing, we clear the cache
+# and retry once before falling back to npm.
 _try_bun_install() {
     local _log _exit_code=0
     _log=$(mktemp)
@@ -249,18 +240,12 @@ if command -v bun &>/dev/null; then
     if _try_bun_install; then
         _bun_install_ok=true
     else
-        # First attempt failed -- try reinstalling bun from official source and retry
-        echo "   Reinstalling bun from bun.sh and retrying..."
-        if curl -fsSL https://bun.sh/install 2>/dev/null | bash > /dev/null 2>&1; then
-            export BUN_INSTALL="${BUN_INSTALL:-$HOME/.bun}"
-            export PATH="$BUN_INSTALL/bin:$PATH"
-            hash -r 2>/dev/null || true
-        fi
-        if command -v bun &>/dev/null; then
-            echo "   bun reinstalled ($(bun --version)), retrying..."
-            if _try_bun_install; then
-                _bun_install_ok=true
-            fi
+        # First attempt failed, likely due to corrupt cache entries.
+        # Clear the cache and retry once.
+        echo "   Clearing bun cache and retrying..."
+        bun pm cache rm > /dev/null 2>&1 || true
+        if _try_bun_install; then
+            _bun_install_ok=true
         fi
     fi
 fi

From bc9cf314786041648a547722660379eb92e6c871 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 07:20:55 -0700
Subject: [PATCH 31/34] Pin torch>=2.4,<2.11.0 in Studio installers (#4595)

torch 2.11.0 has a torch.compile/dynamo bug that causes a
StopIteration crash in dict_keys_getitem when compiling MoE
router functions (e.g. GptOssTopKRouter_forward). Pin to
<2.11.0 until the upstream fix lands.

Applies to both install.sh (Linux/macOS) and install.ps1
(Windows) fresh install paths.
---
 install.ps1 | 2 +-
 install.sh  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/install.ps1 b/install.ps1
index 83576d9c75..a4ed2658c9 100644
--- a/install.ps1
+++ b/install.ps1
@@ -583,7 +583,7 @@ shell.Run cmd, 0, False
         uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.3.11" unsloth-zoo
     } elseif ($TorchIndexUrl) {
         Write-Host "==> Installing PyTorch ($TorchIndexUrl)..."
-        uv pip install --python $VenvPython torch torchvision torchaudio --index-url $TorchIndexUrl
+        uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl
         if ($LASTEXITCODE -ne 0) {
             Write-Host "[ERROR] Failed to install PyTorch (exit code $LASTEXITCODE)" -ForegroundColor Red
             return
diff --git a/install.sh b/install.sh
index ec5008af12..6f60c23d27 100755
--- a/install.sh
+++ b/install.sh
@@ -775,7 +775,7 @@ if [ "$_MIGRATED" = true ]; then
 elif [ -n "$TORCH_INDEX_URL" ]; then
     # Fresh: Step 1 - install torch from explicit index
     echo "==> Installing PyTorch ($TORCH_INDEX_URL)..."
-    uv pip install --python "$_VENV_PY" torch torchvision torchaudio \
+    uv pip install --python "$_VENV_PY" "torch>=2.4,<2.11.0" torchvision torchaudio \
         --index-url "$TORCH_INDEX_URL"
     # Fresh: Step 2 - install unsloth, preserving pre-installed torch
     echo "==> Installing unsloth (this may take a few minutes)..."

From 3efea63e2f52a0061ba37167833ee5fa09b99cc8 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 07:25:47 -0700
Subject: [PATCH 32/34] fix(studio): source-build fallback prefers Unsloth's
 tested tag over upstream latest (#4593)

* fix(studio): source-build fallback prefers Unsloth's tested tag over upstream latest

When the prebuilt install fails and falls back to source build,
--resolve-llama-tag now queries the Unsloth release repo
(unslothai/llama.cpp) first to get the latest tested/approved tag
(e.g. b8508), instead of going straight to ggml-org/llama.cpp which
may return a newer untested tag (e.g. b8514).

This ensures the source-build fallback compiles the same version that
the prebuilt path would have installed, rather than a potentially
incompatible bleeding-edge release.

Resolution order for "latest":
  1. Unsloth release repo (tested/approved)
  2. ggml-org upstream (bleeding-edge)
  3. Raw requested tag string (last resort)

Changes:
- resolve_requested_llama_tag() accepts optional published_repo param
  with docstring explaining the resolution order
- CLI --resolve-llama-tag passes --published-repo through
- setup.sh and setup.ps1 pass --published-repo to --resolve-llama-tag
  with inline comments explaining the preference

* [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/install_llama_prebuilt.py | 34 +++++++++++++++++++++++++++++++-
 studio/setup.ps1                 |  5 ++++-
 studio/setup.sh                  |  5 ++++-
 3 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py
index a9d0b72352..516dc4b6a4 100755
--- a/studio/install_llama_prebuilt.py
+++ b/studio/install_llama_prebuilt.py
@@ -1285,9 +1285,39 @@ def pinned_published_release_bundle(
 
 def resolve_requested_llama_tag(
     requested_tag: str | None,
+    published_repo: str = "",
 ) -> str:
+    """Resolve a llama.cpp tag for source-build fallback.
+
+    Resolution order:
+      1. Concrete tag (e.g. "b8508") -- returned as-is.
+      2. "latest" with published_repo -- query the Unsloth release repo
+         (e.g. unslothai/llama.cpp) for its latest release tag. This is the
+         tested/approved version that matches the prebuilt binaries.
+      3. "latest" without published_repo or if (2) fails -- query the upstream
+         ggml-org/llama.cpp repo. This may return a newer, untested tag.
+
+    The Unsloth repo is preferred because its releases are pinned to specific
+    upstream tags that have been validated with Unsloth Studio. Using the
+    upstream bleeding-edge tag risks API/ABI incompatibilities.
+    """
     if requested_tag and requested_tag != "latest":
         return requested_tag
+    # Prefer the Unsloth release repo tag (tested/approved) over bleeding-edge
+    # upstream. For example, unslothai/llama.cpp may publish b8508 while
+    # ggml-org/llama.cpp latest is b8514. The source-build fallback should
+    # compile the same version the prebuilt path would have installed.
+    if published_repo:
+        try:
+            payload = fetch_json(
+                f"https://api.github.com/repos/{published_repo}/releases/latest"
+            )
+            tag = payload.get("tag_name")
+            if isinstance(tag, str) and tag:
+                return tag
+        except Exception:
+            pass
+    # Fall back to upstream ggml-org latest release tag
     return latest_upstream_release_tag()
 
 
@@ -3360,7 +3390,9 @@ def parse_args() -> argparse.Namespace:
 def main() -> int:
     args = parse_args()
     if args.resolve_llama_tag is not None:
-        print(resolve_requested_llama_tag(args.resolve_llama_tag))
+        # Pass published_repo so the resolver prefers the Unsloth release tag
+        # (tested/approved) over the upstream ggml-org bleeding-edge tag.
+        print(resolve_requested_llama_tag(args.resolve_llama_tag, args.published_repo))
         return EXIT_SUCCESS
 
     if args.resolve_install_tag is not None:
diff --git a/studio/setup.ps1 b/studio/setup.ps1
index d8465fd039..4cd19fdb03 100644
--- a/studio/setup.ps1
+++ b/studio/setup.ps1
@@ -1314,7 +1314,10 @@ if ($resolveExit -ne 0 -or [string]::IsNullOrWhiteSpace($ResolvedLlamaTag)) {
     if ($resolveOutput) {
         $resolveOutput | ForEach-Object { Write-Host $_ }
     }
-    $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-llama-tag $RequestedLlamaTag 2>$null
+    # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo
+    # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream
+    # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp.
+    $fallbackOutput = & python "$PSScriptRoot\install_llama_prebuilt.py" --resolve-llama-tag $RequestedLlamaTag --published-repo $HelperReleaseRepo 2>$null
     $fallbackExit = $LASTEXITCODE
     $ResolvedLlamaTag = if ($fallbackExit -eq 0 -and $fallbackOutput) {
         ($fallbackOutput | Select-Object -Last 1).ToString().Trim()
diff --git a/studio/setup.sh b/studio/setup.sh
index c095fc7245..a7991b83be 100755
--- a/studio/setup.sh
+++ b/studio/setup.sh
@@ -397,7 +397,10 @@ if [ -z "$_RESOLVED_LLAMA_TAG" ]; then
     echo "⚠️  Failed to resolve an installable prebuilt llama.cpp tag via $_HELPER_RELEASE_REPO"
     cat "$_RESOLVE_LLAMA_LOG" >&2 || true
     set +e
-    _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" --resolve-llama-tag "$_REQUESTED_LLAMA_TAG" 2>/dev/null)"
+    # Resolve the llama.cpp tag for source-build fallback. Pass --published-repo
+    # so the resolver prefers Unsloth's tested tag (e.g. b8508) over the upstream
+    # bleeding-edge tag (e.g. b8514) from ggml-org/llama.cpp.
+    _RESOLVED_LLAMA_TAG="$(python "$SCRIPT_DIR/install_llama_prebuilt.py" --resolve-llama-tag "$_REQUESTED_LLAMA_TAG" --published-repo "$_HELPER_RELEASE_REPO" 2>/dev/null)"
     _RESOLVE_UPSTREAM_STATUS=$?
     set -e
     if [ "$_RESOLVE_UPSTREAM_STATUS" -ne 0 ] || [ -z "$_RESOLVED_LLAMA_TAG" ]; then

From 366fb048d4cf13a2868dc45b9e7f8142845de8b9 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 07:27:08 -0700
Subject: [PATCH 33/34] fix(studio): add bun cache validation to Windows
 setup.ps1 (#4596)

Port the bun cache corruption fix from setup.sh to setup.ps1.

bun's package cache can become corrupt, storing only package metadata
without actual content. This causes bun install to exit 0 but leave
binaries like tsc missing from node_modules/.bin/.

Changes:
- After bun install, verify tsc and vite exist in node_modules\.bin\
- Check for both bare names and .cmd wrappers (Windows creates both)
- If missing, clear the bun cache and retry once
- Only fall back to npm if the retry also fails
---
 studio/setup.ps1 | 28 +++++++++++++++++++++++++++-
 1 file changed, 27 insertions(+), 1 deletion(-)

diff --git a/studio/setup.ps1 b/studio/setup.ps1
index 4cd19fdb03..0ac54d3866 100644
--- a/studio/setup.ps1
+++ b/studio/setup.ps1
@@ -919,11 +919,37 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) {
 
     $UseBun = $null -ne (Get-Command bun -ErrorAction SilentlyContinue)
 
+    # bun's package cache can become corrupt -- packages get stored with only
+    # metadata but no actual content (bin/, lib/). When this happens bun install
+    # exits 0 but leaves binaries missing. We validate after install and clear
+    # the cache + retry once before falling back to npm.
     if ($UseBun) {
         Write-Host "   Using bun for package install (faster)" -ForegroundColor DarkGray
         & bun install *> $null
         $bunExit = $LASTEXITCODE
-        if ($bunExit -ne 0) {
+        # On Windows, .bin/ entries can be tsc, tsc.cmd, or tsc.ps1
+        $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd")
+        $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd")
+        if ($bunExit -eq 0 -and $hasTsc -and $hasVite) {
+            # bun install succeeded and critical binaries are present
+        } elseif ($bunExit -eq 0) {
+            Write-Host "   bun install exited 0 but critical binaries are missing, clearing cache and retrying..." -ForegroundColor Yellow
+            if (Test-Path "node_modules") {
+                Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue
+            }
+            & bun pm cache rm *> $null
+            & bun install *> $null
+            $bunExit = $LASTEXITCODE
+            $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd")
+            $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd")
+            if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) {
+                Write-Host "   bun retry failed, falling back to npm" -ForegroundColor Yellow
+                if (Test-Path "node_modules") {
+                    Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue
+                }
+                $UseBun = $false
+            }
+        } else {
             Write-Host "   [WARN] bun install failed (exit $bunExit), falling back to npm" -ForegroundColor Yellow
             if (Test-Path "node_modules") {
                 Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue

From ebe22c1e9ed12a801aee0a5fab4fb87c54de9c18 Mon Sep 17 00:00:00 2001
From: Daniel Han 
Date: Wed, 25 Mar 2026 07:30:40 -0700
Subject: [PATCH 34/34] Update _utils.py

---
 unsloth/models/_utils.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py
index 13acc98ea6..02e2170b70 100644
--- a/unsloth/models/_utils.py
+++ b/unsloth/models/_utils.py
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "2026.3.11"
+__version__ = "2026.3.12"
 
 __all__ = [
     "SUPPORTS_BFLOAT16",